mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-10 15:15:34 +00:00
Continued porting to React
This commit is contained in:
@@ -8,13 +8,15 @@ import Header from './components/Header';
|
||||
import AboutPage from './pages/AboutPage';
|
||||
import NotFoundPage from './pages/NotFoundPage';
|
||||
import OutfittingPage from './pages/OutfittingPage';
|
||||
import ComparisonPage from './pages/ComparisonPage';
|
||||
import ShipyardPage from './pages/ShipyardPage';
|
||||
|
||||
export default class Coriolis extends React.Component {
|
||||
|
||||
static childContextTypes = {
|
||||
language: React.PropTypes.object.isRequired,
|
||||
route: React.PropTypes.object
|
||||
sizeRatio: React.PropTypes.number.isRequired,
|
||||
route: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor() {
|
||||
@@ -24,19 +26,21 @@ export default class Coriolis extends React.Component {
|
||||
this._closeMenu = this._closeMenu.bind(this);
|
||||
this._showModal = this._showModal.bind(this);
|
||||
this._hideModal = this._hideModal.bind(this);
|
||||
this._onLanguageChange = this._onLanguageChange.bind(this)
|
||||
this._onLanguageChange = this._onLanguageChange.bind(this);
|
||||
this._onSizeRatioChange = this._onSizeRatioChange.bind(this)
|
||||
this._keyDown = this._keyDown.bind(this);
|
||||
|
||||
this.state = {
|
||||
page: null,
|
||||
language: getLanguage(Persist.getLangCode()),
|
||||
route: null
|
||||
route: null,
|
||||
sizeRatio: Persist.getSizeRatio()
|
||||
};
|
||||
|
||||
Router('', (r) => this._setPage(ShipyardPage, r));
|
||||
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
|
||||
// Router('/compare/:name', compare);
|
||||
// Router('/comparison/:code', comparison);
|
||||
Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
|
||||
Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
|
||||
Router('/about', (r) => this._setPage(AboutPage, r));
|
||||
Router('*', (r) => this._setPage(null, r));
|
||||
}
|
||||
@@ -54,6 +58,10 @@ export default class Coriolis extends React.Component {
|
||||
this.setState({ language: getLanguage(Persist.getLangCode()) });
|
||||
}
|
||||
|
||||
_onSizeRatioChange(sizeRatio) {
|
||||
this.setState({ sizeRatio });
|
||||
}
|
||||
|
||||
_keyDown(e) {
|
||||
switch (e.keyCode) {
|
||||
case 27:
|
||||
@@ -63,36 +71,59 @@ export default class Coriolis extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the modal display with the specified content
|
||||
* @param {React.Component} content Modal Content
|
||||
*/
|
||||
_showModal(content) {
|
||||
let modal = <div className='modal-bg' onClick={(e) => this._hideModal() }>{content}</div>;
|
||||
this.setState({ modal });
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides any open modal
|
||||
*/
|
||||
_hideModal() {
|
||||
if (this.state.modal) {
|
||||
this.setState({ modal: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the open menu state
|
||||
* @param {string|object} currentMenu The reference to the current menu
|
||||
*/
|
||||
_openMenu(currentMenu) {
|
||||
if (this.state.currentMenu != currentMenu) {
|
||||
this.setState({ currentMenu });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the open menu
|
||||
*/
|
||||
_closeMenu() {
|
||||
if (this.state.currentMenu) {
|
||||
this.setState({ currentMenu: null });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the context to be passed down to pages / components containing
|
||||
* language, sizeRatio and route references
|
||||
* @return {object} Context to be passed down
|
||||
*/
|
||||
getChildContext() {
|
||||
return {
|
||||
language: this.state.language,
|
||||
route: this.state.route
|
||||
route: this.state.route,
|
||||
sizeRatio: this.state.sizeRatio
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds necessary listeners and starts Routing
|
||||
*/
|
||||
componentWillMount() {
|
||||
// Listen for appcache updated event, present refresh to update view
|
||||
if (window.applicationCache) {
|
||||
@@ -107,7 +138,7 @@ export default class Coriolis extends React.Component {
|
||||
window.addEventListener('resize', InterfaceEvents.windowResized);
|
||||
document.addEventListener('keydown', this._keyDown);
|
||||
Persist.addListener('language', this._onLanguageChange);
|
||||
Persist.addListener('language', this._onLanguageChange);
|
||||
Persist.addListener('sizeRatio', this._onSizeRatioChange);
|
||||
InterfaceEvents.addListener('openMenu', this._openMenu);
|
||||
InterfaceEvents.addListener('closeMenu', this._closeMenu);
|
||||
InterfaceEvents.addListener('showModal', this._showModal);
|
||||
@@ -116,6 +147,10 @@ export default class Coriolis extends React.Component {
|
||||
Router.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the main app
|
||||
* @return {React.Component} The main app
|
||||
*/
|
||||
render() {
|
||||
return (
|
||||
<div onClick={this._closeMenu}>
|
||||
|
||||
102
src/app/components/ComparisonTable.jsx
Normal file
102
src/app/components/ComparisonTable.jsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import Link from './Link';
|
||||
import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
|
||||
|
||||
export default class ComparisonTable extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
facets: React.PropTypes.array.isRequired,
|
||||
builds: React.PropTypes.array.isRequired,
|
||||
onSort: React.PropTypes.func.isRequired,
|
||||
predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender
|
||||
desc: React.PropTypes.bool.isRequired, // Used only to test again prop changes for shouldRender
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this._buildHeaders = this._buildHeaders.bind(this);
|
||||
|
||||
this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate);
|
||||
}
|
||||
|
||||
_buildHeaders(facets, onSort, translate) {
|
||||
let header = [
|
||||
<th key='ship' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'name')}>{translate('ship')}</th>,
|
||||
<th key='build' rowSpan='2' className='sortable' onClick={onSort.bind(null, 'buildName')}>{translate('build')}</th>
|
||||
];
|
||||
let subHeader = [];
|
||||
|
||||
for (let f of facets) {
|
||||
if (f.active) {
|
||||
let p = f.props;
|
||||
let pl = p.length;
|
||||
header.push(<th key={f.title} rowSpan={pl === 1 ? 2 : 1} colSpan={pl} className={cn({ sortable: pl === 1 })} onClick={pl === 1 ? onSort.bind(null, p[0]) : null }>
|
||||
{translate(f.title)}
|
||||
</th>);
|
||||
|
||||
if (pl > 1) {
|
||||
for (let i = 0; i < pl; i++) {
|
||||
subHeader.push(<th key={p[i]} className={cn('sortable', { lft: i === 0 } )} onClick={onSort.bind(null, p[i])} >{translate(f.lbls[i])}</th>);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { header, subHeader };
|
||||
}
|
||||
|
||||
_buildRow(build, facets, formats, units) {
|
||||
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
|
||||
let cells = [
|
||||
<td key='s' className='tl'><Link href={url}>{build.name}</Link></td>,
|
||||
<td key='bn' className='tl'><Link href={url}>{build.buildName}</Link></td>
|
||||
];
|
||||
|
||||
for (let f of facets) {
|
||||
if (f.active) {
|
||||
for (let p of f.props) {
|
||||
cells.push(<td key={p}>{formats[f.fmt](build[p])}{f.unit ? units[f.unit] : null}</td>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return <tr key={build.id + build.buildName} className='tr'>{cells}</tr>;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
// If facets or language has changed re-render header
|
||||
if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) {
|
||||
this.setState(this._buildHeaders(nextProps.facets, nextProps.onSort, nextContext.language.translate));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let { builds, facets } = this.props;
|
||||
let { header, subHeader } = this.state;
|
||||
let { formats, units } = this.context.language;
|
||||
|
||||
let buildsRows = new Array(builds.length);
|
||||
|
||||
for (let i = 0, l = buildsRows.length; i < l; i++) {
|
||||
buildsRows[i] = this._buildRow(builds[i], facets, formats, units);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='scroll-x'>
|
||||
<table id='comp-tbl'>
|
||||
<thead>
|
||||
<tr className='main'>{header}</tr>
|
||||
<tr>{subHeader}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{buildsRows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,28 +11,25 @@ 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);
|
||||
this._sortCost = this._sortCost.bind(this);
|
||||
this._sortAmmo = this._sortAmmo.bind(this);
|
||||
this._sortRetrofit = this._sortRetrofit.bind(this);
|
||||
this._buildRetrofitShip = this._buildRetrofitShip.bind(this);
|
||||
this._onBaseRetrofitChange = this._onBaseRetrofitChange.bind(this);
|
||||
this._defaultRetrofitName = this._defaultRetrofitName.bind(this);
|
||||
|
||||
let data = Ships[props.shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||
let retrofitName = props.buildName;
|
||||
let data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults
|
||||
let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName);
|
||||
let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName);
|
||||
let shipDiscount = Persist.getShipDiscount();
|
||||
let moduleDiscount = Persist.getModuleDiscount();
|
||||
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);
|
||||
@@ -45,16 +42,35 @@ export default class CostSection extends TranslatedComponent {
|
||||
total: props.ship.totalCost,
|
||||
insurance: Insurance[Persist.getInsurance()],
|
||||
tab: Persist.getCostTab(),
|
||||
buildOptions: Persist.getBuildsNamesFor(props.shipId),
|
||||
ammoPredicate: 'module',
|
||||
buildOptions: Persist.getBuildsNamesFor(props.ship.id),
|
||||
ammoPredicate: 'cr',
|
||||
ammoDesc: true,
|
||||
costPredicate: 'cr',
|
||||
costDesc: true,
|
||||
retroPredicate: 'module',
|
||||
retroPredicate: 'cr',
|
||||
retroDesc: true
|
||||
};
|
||||
}
|
||||
|
||||
_buildRetrofitShip(shipId, name, retrofitShip) {
|
||||
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
|
||||
|
||||
if (!retrofitShip) {
|
||||
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
|
||||
}
|
||||
|
||||
if (Persist.hasBuild(shipId, name)) {
|
||||
retrofitShip.buildFrom(Persist.getBuild(shipId, name)); // Populate modules from existing build
|
||||
} else {
|
||||
retrofitShip.buildWith(data.defaults); // Populate with default components
|
||||
}
|
||||
return retrofitShip;
|
||||
}
|
||||
|
||||
_defaultRetrofitName(shipId, name) {
|
||||
return Persist.hasBuild(shipId, name) ? name : null;
|
||||
}
|
||||
|
||||
_showTab(tab) {
|
||||
Persist.setCostTab(tab);
|
||||
this.setState({ tab });
|
||||
@@ -72,72 +88,121 @@ export default class CostSection extends TranslatedComponent {
|
||||
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
|
||||
/**
|
||||
* Repopulate modules on retrofit ship from existing build
|
||||
* @param {string} retrofitName Build name to base the retrofit ship on
|
||||
*/
|
||||
_onBaseRetrofitChange(event) {
|
||||
let retrofitName = event.target.value;
|
||||
let ship = this.props.ship;
|
||||
|
||||
if (retrofitName) {
|
||||
this.state.retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName));
|
||||
} else {
|
||||
this.state.retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
|
||||
}
|
||||
this._updateRetrofit(ship, this.state.retrofitShip);
|
||||
this.setState({ retrofitName });
|
||||
}
|
||||
|
||||
_onBuildSaved(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
|
||||
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||
} else {
|
||||
this.setState({buildOptions: Persist.getBuildsNamesFor(this.props.shipId) });
|
||||
this.setState({ buildOptions: Persist.getBuildsNamesFor(this.props.shipId) });
|
||||
}
|
||||
}
|
||||
|
||||
_onBuildDeleted(shipId, name, code) {
|
||||
if(this.state.retrofitName == name) {
|
||||
this.state.retrofitShip.buildWith(Ships[shipId].defaults); // Retrofit ship becomes stock build
|
||||
this.setState({ retrofitName: null });
|
||||
}
|
||||
this.setState({ buildOptions: Persist.getBuildsNamesFor(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;
|
||||
_toggleRetrofitCost(item) {
|
||||
let retrofitTotal = this.state.retrofitTotal;
|
||||
item.retroItem.incCost = !item.retroItem.incCost;
|
||||
retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1);
|
||||
this.setState({ retrofitTotal });
|
||||
}
|
||||
|
||||
_sortCostBy(predicate) {
|
||||
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();
|
||||
if (costPredicate == predicate) {
|
||||
costDesc = !costDesc;
|
||||
}
|
||||
|
||||
this.setState({ costPredicate: predicate, costDesc });
|
||||
}
|
||||
|
||||
_sortAmmo(predicate) {
|
||||
let { ammoPredicate, ammoDesc, ammoCosts } = this.state;
|
||||
_sortCost(ship, predicate, desc) {
|
||||
let costList = ship.costList;
|
||||
|
||||
if (predicate == 'm') {
|
||||
costList.sort(nameComparator(this.context.language.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 (!desc) {
|
||||
costList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
_sortAmmoBy(predicate) {
|
||||
let { ammoPredicate, ammoDesc } = 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]);
|
||||
this.setState({ ammoPredicate: predicate, ammoDesc });
|
||||
}
|
||||
|
||||
_sortAmmo(ammoCosts, predicate, desc) {
|
||||
|
||||
if (predicate == 'm') {
|
||||
ammoCosts.sort(nameComparator(this.context.language.translate));
|
||||
} else {
|
||||
ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
|
||||
}
|
||||
|
||||
if (!ammoDesc) {
|
||||
if (!desc) {
|
||||
ammoCosts.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ ammoPredicate: predicate, ammoDesc });
|
||||
_sortRetrofitBy(predicate) {
|
||||
let { retroPredicate, retroDesc } = this.state;
|
||||
|
||||
if (retroPredicate == predicate) {
|
||||
retroDesc = !retroDesc;
|
||||
}
|
||||
|
||||
this.setState({ retroPredicate: predicate, retroDesc });
|
||||
}
|
||||
|
||||
_sortRetrofit(retrofitCosts, predicate, desc) {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (predicate == 'cr') {
|
||||
retrofitCosts.sort((a, b) => a.netCost - b.netCost);
|
||||
} else {
|
||||
retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : ''));
|
||||
}
|
||||
|
||||
if (!desc) {
|
||||
retrofitCosts.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
_costsTab() {
|
||||
@@ -162,12 +227,12 @@ export default class CostSection extends TranslatedComponent {
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortCost.bind(this,'m')}>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortCostBy.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>}
|
||||
{shipDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct1(1 - shipDiscount)}]`}</u>}
|
||||
{moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}</u>}
|
||||
</th>
|
||||
<th className='sortable le' onClick={this._sortCost.bind(this, 'cr')} >{translate('credits')}</th>
|
||||
<th className='sortable le' onClick={this._sortCostBy.bind(this, 'cr')} >{translate('credits')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -185,11 +250,72 @@ export default class CostSection extends TranslatedComponent {
|
||||
</div>;
|
||||
}
|
||||
|
||||
updateRetrofitCosts() {
|
||||
var costs = $scope.retrofitList = [];
|
||||
var total = 0, i, l, item;
|
||||
_retrofitTab() {
|
||||
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
|
||||
let { translate, formats, units } = this.context.language;
|
||||
let int = formats.int;
|
||||
let rows = [], options = [<option key='stock' value=''>{translate('Stock')}</option>];
|
||||
|
||||
if (ship.bulkheads.id != retrofitShip.bulkheads.id) {
|
||||
for (let opt of this.state.buildOptions) {
|
||||
options.push(<option key={opt} value={opt}>{opt}</option>);
|
||||
}
|
||||
|
||||
if (retrofitCosts.length) {
|
||||
for (let i = 0, l = retrofitCosts.length; i < l; i++) {
|
||||
let item = retrofitCosts[i];
|
||||
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.retroItem.incCost })} onClick={this._toggleRetrofitCost.bind(this, item)}>
|
||||
<td style={{ width: '1em' }}>{item.sellClassRating}</td>
|
||||
<td className='le shorten cap'>{translate(item.sellName)}</td>
|
||||
<td style={{ width: '1em' }}>{item.buyClassRating}</td>
|
||||
<td className='le shorten cap'>{translate(item.buyName)}</td>
|
||||
<td colSpan='2' className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{int(item.netCost)}{units.CR}</td>
|
||||
</tr>);
|
||||
}
|
||||
} else {
|
||||
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className='scroll-x'>
|
||||
<table style={{ width: '100%' }}>
|
||||
<thead>
|
||||
<tr className='main'>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'sellName')}>{translate('sell')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
|
||||
{translate('net cost')}
|
||||
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
<tr className='ri'>
|
||||
<td colSpan='4' className='lbl' >{translate('cost')}</td>
|
||||
<td colSpan='2' className={cn('val', retrofitTotal > 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
|
||||
{int(retrofitTotal)}{units.CR}
|
||||
</td>
|
||||
</tr>
|
||||
<tr className='ri'>
|
||||
<td colSpan='4' className='lbl cap' >{translate('retrofit from')}</td>
|
||||
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>▾</u></td>
|
||||
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
|
||||
<select style={{ width: '100%', padding: 0 }} value={retrofitName} onChange={this._onBaseRetrofitChange}>
|
||||
{options}
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
_updateRetrofit(ship, retrofitShip) {
|
||||
let retrofitCosts = [];
|
||||
var retrofitTotal = 0, i, l, item;
|
||||
|
||||
if (ship.bulkheads.index != retrofitShip.bulkheads.index) {
|
||||
item = {
|
||||
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
|
||||
buyName: ship.bulkheads.m.name,
|
||||
@@ -198,9 +324,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
|
||||
retroItem: retrofitShip.bulkheads
|
||||
};
|
||||
costs.push(item);
|
||||
retrofitCosts.push(item);
|
||||
if (retrofitShip.bulkheads.incCost) {
|
||||
total += item.netCost;
|
||||
retrofitTotal += item.netCost;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,71 +334,28 @@ export default class CostSection extends TranslatedComponent {
|
||||
var retroSlotGroup = retrofitShip[g];
|
||||
var slotGroup = ship[g];
|
||||
for (i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].id != retroSlotGroup[i].id) {
|
||||
if (slotGroup[i].m != retroSlotGroup[i].m) {
|
||||
item = { netCost: 0, retroItem: retroSlotGroup[i] };
|
||||
if (slotGroup[i].id) {
|
||||
if (slotGroup[i].m) {
|
||||
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) {
|
||||
if (retroSlotGroup[i].m) {
|
||||
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
|
||||
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
|
||||
item.netCost -= retroSlotGroup[i].discountedCost;
|
||||
}
|
||||
costs.push(item);
|
||||
retrofitCosts.push(item);
|
||||
if (retroSlotGroup[i].incCost) {
|
||||
total += item.netCost;
|
||||
retrofitTotal += 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'>▾</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>;
|
||||
this.setState({ retrofitCosts, retrofitTotal });
|
||||
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
|
||||
}
|
||||
|
||||
_ammoTab() {
|
||||
@@ -291,16 +374,16 @@ export default class CostSection extends TranslatedComponent {
|
||||
<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' }}>
|
||||
<table style={{ width: '100%' }}>
|
||||
<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>
|
||||
<th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'm')} >{translate('module')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
|
||||
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
|
||||
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('total cost')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -318,14 +401,13 @@ export default class CostSection extends TranslatedComponent {
|
||||
/**
|
||||
* Recalculate all ammo costs
|
||||
*/
|
||||
_updateAmmoCosts() {
|
||||
let ship = this.props.ship;
|
||||
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, scoop = false;
|
||||
_updateAmmoCosts(ship) {
|
||||
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
|
||||
|
||||
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
|
||||
let slotGroup = ship[g];
|
||||
for (let i = 0, l = slotGroup.length; i < l; i++) {
|
||||
if (slotGroup[i].id) {
|
||||
if (slotGroup[i].m) {
|
||||
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
|
||||
q = 0;
|
||||
switch (slotGroup[i].m.grp) {
|
||||
@@ -338,6 +420,9 @@ export default class CostSection extends TranslatedComponent {
|
||||
case 'am':
|
||||
q = slotGroup[i].m.ammo;
|
||||
break;
|
||||
case 'pv':
|
||||
srvs += slotGroup[i].m.vehicles;
|
||||
break;
|
||||
case 'fx': case 'hb': case 'cc': case 'pc':
|
||||
limpets = ship.cargoCapacity;
|
||||
break;
|
||||
@@ -370,6 +455,17 @@ export default class CostSection extends TranslatedComponent {
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
|
||||
if (srvs > 0) {
|
||||
item = {
|
||||
m: { name: 'SRVs', class: '', rating: '' },
|
||||
max: srvs,
|
||||
cost: 6005,
|
||||
total: srvs * 6005
|
||||
};
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
//calculate refuel costs if no scoop present
|
||||
if (!scoop) {
|
||||
item = {
|
||||
@@ -381,26 +477,66 @@ export default class CostSection extends TranslatedComponent {
|
||||
ammoCosts.push(item);
|
||||
ammoTotal += item.total;
|
||||
}
|
||||
|
||||
this.setState({ ammoTotal, ammoCosts });
|
||||
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.listeners = [
|
||||
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
|
||||
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
|
||||
Persist.addListener('buildSaved', this._onBuildSaved.bind(this)),
|
||||
Persist.addListener('buildDeleted', this._onBuildDeleted.bind(this))
|
||||
];
|
||||
this._updateAmmoCosts(this.props.ship);
|
||||
this._sortCost.call(this, this.state.costPredicate);
|
||||
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
|
||||
this._sortCost(this.props.ship);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
this._updateAmmoCosts(nextProps.ship);
|
||||
let retrofitShip = this.state.retrofitShip;
|
||||
|
||||
this._sortCost();
|
||||
if (nextProps.ship != this.props.ship) { // Ship has changed
|
||||
let nextId = nextProps.ship.id;
|
||||
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
|
||||
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null );
|
||||
this.setState({
|
||||
retrofitShip,
|
||||
retrofitName,
|
||||
buildOptions: Persist.getBuildsNamesFor(nextId)
|
||||
});
|
||||
}
|
||||
|
||||
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
|
||||
this._updateAmmoCosts(nextProps.ship);
|
||||
this._updateRetrofit(nextProps.ship, retrofitShip);
|
||||
this._sortCost(nextProps.ship);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
let state = this.state;
|
||||
|
||||
switch (nextState.tab) {
|
||||
case 'ammo':
|
||||
if (state.ammoPredicate != nextState.ammoPredicate || state.ammoDesc != nextState.ammoDesc) {
|
||||
this._sortAmmo(nextState.ammoCosts, nextState.ammoPredicate, nextState.ammoDesc);
|
||||
}
|
||||
break;
|
||||
case 'retrofit':
|
||||
if (state.retroPredicate != nextState.retroPredicate || state.retroDesc != nextState.retroDesc) {
|
||||
this._sortRetrofit(nextState.retrofitCosts, nextState.retroPredicate, nextState.retroDesc);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (state.costPredicate != nextState.costPredicate || state.costDesc != nextState.costDesc) {
|
||||
this._sortCost(nextProps.ship, nextState.costPredicate, nextState.costDesc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
// remove window listener
|
||||
this.listeners.forEach(l => l.remove());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import { Languages } from '../i18n/Language';
|
||||
import { Insurance, Discounts } from '../shipyard/Constants';
|
||||
import Link from './Link';
|
||||
import ActiveLink from './ActiveLink';
|
||||
import cn from 'classnames';
|
||||
@@ -7,28 +9,58 @@ import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { toDetailedExport } from '../shipyard/Serializer';
|
||||
import ModalDeleteAll from './ModalDeleteAll';
|
||||
import ModalExport from './ModalExport';
|
||||
import ModalImport from './ModalImport';
|
||||
import Slider from './Slider';
|
||||
|
||||
const SIZE_MIN = 0.65;
|
||||
const SIZE_RANGE = 0.55;
|
||||
|
||||
export default class Header extends TranslatedComponent {
|
||||
|
||||
constructor(props) {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
this.shipOrder = Object.keys(Ships).sort();
|
||||
|
||||
this._setLanguage = this._setLanguage.bind(this);
|
||||
this._setInsurance = this._setInsurance.bind(this);
|
||||
|
||||
this.languageOptions = [];
|
||||
this.insuranceOptions = [];
|
||||
this.discountOptions = [];
|
||||
|
||||
let translate = context.language.translate;
|
||||
|
||||
for (let langCode in Languages) {
|
||||
this.languageOptions.push(<option key={langCode} value={langCode}>{Languages[langCode]}</option>);
|
||||
}
|
||||
|
||||
for (let name in Insurance) {
|
||||
this.insuranceOptions.push(<option key={name} value={name}>{translate(name)}</option>);
|
||||
}
|
||||
|
||||
for (let name in Discounts) {
|
||||
this.discountOptions.push(<option key={name} value={Discounts[name]}>{name}</option>);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_setInsurance(e) {
|
||||
e.stopPropagation();
|
||||
Persist.setInsurance('beta'); // TODO: get insurance name
|
||||
Persist.setInsurance(e.target.value);
|
||||
}
|
||||
|
||||
_setModuleDiscount(e) {
|
||||
e.stopPropagation();
|
||||
Persist.setModuleDiscount(0); // TODO: get module discount
|
||||
Persist.setModuleDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
_setShipDiscount(e) {
|
||||
e.stopPropagation();
|
||||
Persist.setShipDiscount(0); // TODO: get ship discount
|
||||
Persist.setShipDiscount(e.target.value * 1);
|
||||
}
|
||||
|
||||
_setLanguage(e){
|
||||
Persist.setLangCode(e.target.value);
|
||||
}
|
||||
|
||||
_showDeleteAll(e) {
|
||||
@@ -37,29 +69,33 @@ export default class Header extends TranslatedComponent {
|
||||
};
|
||||
|
||||
_showBackup(e) {
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
/*$state.go('modal.export', {
|
||||
title: 'backup',
|
||||
data: Persist.getAll(),
|
||||
description: 'PHRASE_BACKUP_DESC'
|
||||
});*/
|
||||
// TODO: implement modal
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
title={translate('backup')}
|
||||
description={translate('PHRASE_BACKUP_DESC')}
|
||||
data={Persist.getAll()}
|
||||
/>);
|
||||
};
|
||||
|
||||
_showDetailedExport(e){
|
||||
let translate = this.context.language.translate;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
/*$state.go('modal.export', {
|
||||
title: 'detailed export',
|
||||
data: Serializer.toDetailedExport(Persist.getBuilds()),
|
||||
description: 'PHRASE_EXPORT_DESC'
|
||||
});*/
|
||||
// TODO: implement modal
|
||||
InterfaceEvents.showModal(<ModalExport
|
||||
title={translate('detailed export')}
|
||||
description={translate('PHRASE_EXPORT_DESC')}
|
||||
data={toDetailedExport(Persist.getBuilds())}
|
||||
/>);
|
||||
}
|
||||
|
||||
_showImport(e) {
|
||||
e.preventDefault();
|
||||
InterfaceEvents.showModal(<ModalImport/>);
|
||||
}
|
||||
|
||||
_setTextSize(size) {
|
||||
Persist.setSizeRatio(size); // TODO: implement properly
|
||||
Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN);
|
||||
}
|
||||
|
||||
_resetTextSize() {
|
||||
@@ -117,9 +153,8 @@ export default class Header extends TranslatedComponent {
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
if (Persist.hasComparisons()) {
|
||||
let comparisons = [];
|
||||
comparisons = [];
|
||||
let comps = Object.keys(Persist.getComparisons()).sort();
|
||||
console.log(comps);
|
||||
|
||||
for (let name of comps) {
|
||||
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className={'block name'}>{name}</ActiveLink>);
|
||||
@@ -145,38 +180,54 @@ export default class Header extends TranslatedComponent {
|
||||
<div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }>
|
||||
<ul>
|
||||
{translate('language')}
|
||||
<li><select className={'cap'} ng-model="language.current" ng-options="langCode as langName for (langCode,langName) in language.opts" ng-change="changeLanguage()"></select></li>
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getLangCode()} onChange={this._setLanguage}>
|
||||
{this.languageOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('insurance')}
|
||||
<li><select className={'cap'} ng-model="insurance.current" ng-options="ins.name | translate for (i,ins) in insurance.opts" ng-change="updateInsurance()"></select></li>
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getInsurance()} onChange={this._setInsurance}>
|
||||
{this.insuranceOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('ship')} {translate('discount')}
|
||||
<li><select className={'cap'} ng-model="discounts.ship" ng-options="i for (i,d) in discounts.opts" ng-change="updateDiscount()"></select></li>
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getShipDiscount()} onChange={this._setShipDiscount}>
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul><br/>
|
||||
<ul>
|
||||
{translate('component')} {translate('discount')}
|
||||
<li><select className={'cap'} ng-model="discounts.components" ng-options="i for (i,d) in discounts.opts" ng-change="updateDiscount()"></select></li>
|
||||
{translate('module')} {translate('discount')}
|
||||
<li>
|
||||
<select className={'cap'} value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
|
||||
{this.discountOptions}
|
||||
</select>
|
||||
</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<ul>
|
||||
{translate('builds')} & {translate('comparisons')}
|
||||
<li><a href="#" className={'block'} ng-click="backup($event)">{translate('backup')}</a></li>
|
||||
<li><a href="#" className={'block'} ng-click="detailedExport($event)">{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className={'block'} ui-sref="modal.import">{translate('import')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
|
||||
<li><a href="#" className={'block'} onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
|
||||
<li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li>
|
||||
</ul>
|
||||
<hr />
|
||||
<table style={{width: 300, backgroundColor: 'transparent'}}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style={{width: 20}}><u>A</u></td>
|
||||
<td slider min="0.65" def="sizeRatio" max="1.2" on-change="textSizeChange(val)" ignore-resize="true"></td>
|
||||
<td style={{width: 20}}><span style={{fontSize: 30}}>A</span></td>
|
||||
<td style={{ width: '1em', verticalAlign: 'top' }}><u>A</u></td>
|
||||
<td><Slider onChange={this._setTextSize} percent={(Persist.getSizeRatio() - SIZE_MIN) / SIZE_RANGE} /></td>
|
||||
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td><td style={{ textAlign: 'center' }} className={'primary-disabled cap'} ng-click="resetTextSize()" translate="reset"></td><td></td>
|
||||
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className={'primary-disabled cap'} onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -186,6 +237,22 @@ export default class Header extends TranslatedComponent {
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
Persist.addListener('language', () => this.forceUpdate());
|
||||
Persist.addListener('insurance', () => this.forceUpdate());
|
||||
Persist.addListener('discounts', () => this.forceUpdate());
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if(this.context.language != nextContext.language) {
|
||||
let translate = nextContext.language.translate;
|
||||
this.insuranceOptions = [];
|
||||
for (let name in Insurance) {
|
||||
this.insuranceOptions.push(<option key={name} value={name}>{translate(name)}</option>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let openedMenu = this.props.currentMenu;
|
||||
|
||||
@@ -16,6 +16,7 @@ export default class InternalSlot extends Slot {
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
|
||||
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
|
||||
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
|
||||
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs} {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
|
||||
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
|
||||
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
|
||||
|
||||
@@ -83,7 +83,6 @@ export default class InternalSlotSection extends SlotSection {
|
||||
enabled={s.enabled}
|
||||
m={s.m}
|
||||
fuel={fuelCapacity}
|
||||
shipMass={ladenMass}
|
||||
/>);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import d3 from 'd3';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
|
||||
const RENDER_POINTS = 20; // Only render 20 points on the graph
|
||||
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 }
|
||||
|
||||
export default class LineChart extends TranslatedComponent {
|
||||
|
||||
@@ -12,40 +15,201 @@ export default class LineChart extends TranslatedComponent {
|
||||
}
|
||||
|
||||
static PropTypes = {
|
||||
xMax: React.PropTypes.number.isRequired,
|
||||
yMax: React.PropTypes.number.isRequired,
|
||||
width: React.PropTypes.number.isRequired,
|
||||
func: React.PropTypes.func.isRequired,
|
||||
xLabel: React.PropTypes.string.isRequired,
|
||||
xMin: React.PropTypes.number,
|
||||
xMax: React.PropTypes.number.isRequired,
|
||||
xUnit: React.PropTypes.string.isRequired,
|
||||
yLabel: React.PropTypes.string.isRequired,
|
||||
yMin: React.PropTypes.number,
|
||||
yMax: React.PropTypes.number.isRequired,
|
||||
yUnit: React.PropTypes.string.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) {
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
// init
|
||||
this._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateSeriesData = this._updateSeriesData.bind(this);
|
||||
this._tooltip = this._tooltip.bind(this);
|
||||
this._showTip = this._showTip.bind(this);
|
||||
this._hideTip = this._hideTip.bind(this);
|
||||
this._moveTip = this._moveTip.bind(this);
|
||||
|
||||
let markerElems = [];
|
||||
let detailElems = [<text key={'lbl'} className='label x' y='1.25em'/>];
|
||||
let xScale = d3.scale.linear();
|
||||
let yScale = d3.scale.linear();
|
||||
let series = props.series;
|
||||
let seriesLines = [];
|
||||
|
||||
this.xAxis = d3.svg.axis().scale(xScale).outerTickSize(0).orient('bottom');
|
||||
this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left');
|
||||
|
||||
for(let i = 0, l = series ? series.length : 1; i < l; i++) {
|
||||
let yAccessor = series ? function(d) { return yScale(d[1][this]); }.bind(series[i]) : (d) => yScale(d[1]);
|
||||
seriesLines.push(d3.svg.line().x((d) => xScale(d[0])).y(yAccessor));
|
||||
detailElems.push(<text key={i} className='label y' y={1.25 * (i + 2) + 'em'}/>);
|
||||
markerElems.push(<circle key={i} className='marker' r='4' />);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
xScale,
|
||||
yScale,
|
||||
seriesLines,
|
||||
detailElems,
|
||||
markerElems,
|
||||
tipHeight: 2 + (1.25 * (series ? series.length : 0.75))
|
||||
};
|
||||
}
|
||||
|
||||
_tooltip(xPos) {
|
||||
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
|
||||
let { xScale, yScale } = this.state;
|
||||
let { formats, translate } = this.context.language;
|
||||
let x0 = xScale.invert(xPos),
|
||||
y0 = func(x0),
|
||||
tips = this.tipContainer,
|
||||
yTotal = 0,
|
||||
flip = (x0 / xScale.domain()[1] > 0.65),
|
||||
tipWidth = 0,
|
||||
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
|
||||
|
||||
tips.selectAll('text.label.y').text(function(d, i) {
|
||||
let yVal = series ? y0[series[i]] : y0;
|
||||
yTotal += yVal;
|
||||
return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal);
|
||||
}).append('tspan').attr('class', 'metric').text(' ' + yUnit);
|
||||
|
||||
tips.selectAll('text').each(function() {
|
||||
if (this.getBBox().width > tipWidth) {
|
||||
tipWidth = Math.ceil(this.getBBox().width);
|
||||
}
|
||||
});
|
||||
|
||||
let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2));
|
||||
|
||||
tipWidth += 8;
|
||||
tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')');
|
||||
tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start');
|
||||
tips.selectAll('text.label.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit);
|
||||
tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).style('text-anchor', flip ? 'end' : 'start');
|
||||
this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0));
|
||||
}
|
||||
|
||||
_updateDimensions(props, sizeRatio) {
|
||||
let { width, xMax, xMin, yMin, yMax } = props;
|
||||
let innerWidth = width - MARGIN.left - MARGIN.right;
|
||||
let outerHeight = Math.round(width * 0.5 * sizeRatio);
|
||||
let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
|
||||
|
||||
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
|
||||
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]);
|
||||
this.setState({ innerWidth, outerHeight, innerHeight });
|
||||
}
|
||||
|
||||
_showTip(e) {
|
||||
this._moveTip(e);
|
||||
this.tipContainer.style('display', null);
|
||||
this.markersContainer.style('display', null);
|
||||
}
|
||||
|
||||
_moveTip(e) {
|
||||
this._tooltip(Math.round(e.clientX - e.target.getBoundingClientRect().left));
|
||||
}
|
||||
|
||||
_hideTip() {
|
||||
this.tipContainer.style('display', 'none');
|
||||
this.markersContainer.style('display', 'none');
|
||||
}
|
||||
|
||||
_updateSeriesData(props) {
|
||||
let { func, xMin, xMax, series } = props;
|
||||
let delta = (xMax - xMin) / RENDER_POINTS;
|
||||
let seriesData = new Array(RENDER_POINTS);
|
||||
|
||||
if (delta) {
|
||||
seriesData = new Array(RENDER_POINTS);
|
||||
for (let i = 0, x = xMin; i < RENDER_POINTS; i++) {
|
||||
seriesData[i] = [ x, func(x) ];
|
||||
x += delta;
|
||||
}
|
||||
seriesData[RENDER_POINTS - 1] = [ xMax, func(xMax) ];
|
||||
} else {
|
||||
let yVal = func(xMin);
|
||||
seriesData = [ [0, yVal], [1, yVal]];
|
||||
}
|
||||
|
||||
this.setState({ seriesData });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
// Listen to window resize
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
// remove window listener
|
||||
// remove mouse move listener / touch listner?
|
||||
this._updateDimensions(this.props, this.context.sizeRatio);
|
||||
this._updateSeriesData(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
// on language change update formatting
|
||||
let { func, xMin, xMax, yMin, yMax, width } = nextProps;
|
||||
let props = this.props;
|
||||
|
||||
let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func;
|
||||
|
||||
if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
|
||||
if (domainChanged) {
|
||||
this._updateSeriesData(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div></div>;
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let { xLabel, yLabel, xUnit, yUnit, colors } = this.props;
|
||||
let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
|
||||
let line = this.line;
|
||||
let lines = seriesLines.map((line, i) => <path key={i} className='line' stroke={colors[i]} strokeWidth='2' d={line(seriesData)} />);
|
||||
|
||||
return <svg style={{ width: '100%', height: outerHeight }}>
|
||||
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
|
||||
<g>{lines}</g>
|
||||
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
|
||||
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{xLabel}</tspan>
|
||||
<tspan className='metric'>{` (${xUnit})`}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
|
||||
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
|
||||
<tspan>{yLabel}</tspan>
|
||||
<tspan className='metric'>{` (${yUnit})`}</tspan>
|
||||
</text>
|
||||
</g>
|
||||
<g ref={(g) => this.tipContainer = d3.select(g)} className='tooltip' style={{ display: 'none' }}>
|
||||
<rect className='tip' style={{height: tipHeight + 'em'}}></rect>
|
||||
{detailElems}
|
||||
</g>
|
||||
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
|
||||
{markerElems}
|
||||
</g>
|
||||
<rect
|
||||
fillOpacity='0'
|
||||
height={innerHeight}
|
||||
width={innerWidth + 1}
|
||||
onMouseEnter={this._showTip}
|
||||
onTouchStart={this._showTip}
|
||||
onMouseLeave={this._hideTip}
|
||||
onTouchEnd={this._hideTip}
|
||||
onMouseMove={this._moveTip}
|
||||
onTouchMove={this._moveTip}
|
||||
/>
|
||||
</g>
|
||||
</svg>;
|
||||
}
|
||||
}
|
||||
|
||||
108
src/app/components/ModalCompare.jsx
Normal file
108
src/app/components/ModalCompare.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Persist from '../stores/Persist';
|
||||
|
||||
function buildComparator(a, b) {
|
||||
if (a.name == b.name) {
|
||||
return a.buildName > b.buildName;
|
||||
}
|
||||
return a.name > b.name;
|
||||
}
|
||||
|
||||
|
||||
export default class ModalCompare extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
onSelect: React.PropTypes.func.isRequired,
|
||||
builds: React.PropTypes.array
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
builds: []
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
let builds = props.builds;
|
||||
let allBuilds = Persist.getBuilds();
|
||||
let unusedBuilds = [];
|
||||
|
||||
for (let id in allBuilds) {
|
||||
for (let buildName in allBuilds[id]) {
|
||||
if (!builds.find((e) => e.buildName == buildName && e.id == id)) {
|
||||
unusedBuilds.push({ id, buildName, name: Ships[id].properties.name })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builds.sort(buildComparator);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.state = { builds, unusedBuilds };
|
||||
}
|
||||
|
||||
_addBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
builds.push(unusedBuilds[buildIndex]);
|
||||
unusedBuilds = unusedBuilds.splice(buildIndex, 1);
|
||||
builds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
}
|
||||
|
||||
_removeBuild(buildIndex) {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
unusedBuilds.push(builds[buildIndex]);
|
||||
builds = builds.splice(buildIndex, 1);
|
||||
unusedBuilds.sort(buildComparator);
|
||||
|
||||
this.setState({ builds, unusedBuilds });
|
||||
}
|
||||
|
||||
_selectBuilds() {
|
||||
this.props.onSelect(this.state.builds);
|
||||
}
|
||||
|
||||
render() {
|
||||
let { builds, unusedBuilds } = this.state;
|
||||
let translate = this.context.language.translate;
|
||||
|
||||
let availableBuilds = unusedBuilds.map((build, i) =>
|
||||
<tr key={i} onClick={this._addBuild.bind(this, i)}>
|
||||
<td className='tl'>{build.name}</td>
|
||||
<td className='tl'>{build.buildName}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
let selectedBuilds = builds.map((build, i) =>
|
||||
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
|
||||
<td className='tl'>{build.name}</td><
|
||||
td className='tl'>{build.buildName}</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
return <div className='modal' onClick={ (e) => e.stopPropagation() }>
|
||||
<h3>{translate('PHRASE_SELECT_BUILDS')}</h3>
|
||||
<div id='build-select'>
|
||||
<table>
|
||||
<thead><tr><th colSpan='2'>{translate('available')}</th></tr></thead>
|
||||
<tbody>
|
||||
{availableBuilds}
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>⇆</h1>
|
||||
<table>
|
||||
<thead><tr><th colSpan='2'>{translate('added')}</th></tr></thead>
|
||||
<tbody>
|
||||
{selectedBuilds}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
<button className='cap' onClick={this._selectBuilds.bind(this)}>{translate('Ok')}</button>
|
||||
<button className='r cap' onClick={() => InterfaceEvents.hideModal()}>{translate('Cancel')}</button>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ 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])
|
||||
data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array])
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import Ships from '../shipyard/Ships';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { Download } from './SvgIcons';
|
||||
@@ -36,7 +36,8 @@ function validateBuild(shipId, code, name) {
|
||||
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||
}
|
||||
try {
|
||||
Serializer.toShip(new Ship(shipId, shipData.properties, shipData.slots), code);
|
||||
let ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
ship.buildFrom(code);
|
||||
} catch (e) {
|
||||
throw shipData.properties.name + ' build "' + name + '" is not valid!';
|
||||
}
|
||||
@@ -58,17 +59,11 @@ function detailedJsonToBuild(detailedBuild) {
|
||||
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data';
|
||||
}
|
||||
|
||||
return { shipId: ship.id, name: detailedBuild.name, code: Serializer.fromShip(ship) };
|
||||
return { shipId: ship.id, name: detailedBuild.name, code: ship.toString() };
|
||||
}
|
||||
|
||||
export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: React.propTypes.string,
|
||||
promise: React.propTypes.func,
|
||||
data: React.propTypes.oneOfType[React.propTypes.string, React.propTypes.object]
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -84,7 +79,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
};
|
||||
|
||||
this._process = this._process.bind(this);
|
||||
this._import = this._import.bind(this);
|
||||
this._importBackup = this._importBackup.bind(this);
|
||||
this._importDetailedArray = this._importDetailedArray.bind(this);
|
||||
this._importTextBuild = this._importTextBuild.bind(this);
|
||||
this._validateImport = this._validateImport.bind(this);
|
||||
}
|
||||
|
||||
_importBackup(importData) {
|
||||
@@ -121,7 +120,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
_importDetailedArray(importArr) {
|
||||
let builds = {};
|
||||
for (let i = 0, l = importArr.length; i < l; i++) {
|
||||
let build = this._detailedJsonToBuild(importArr[i]);
|
||||
let build = detailedJsonToBuild(importArr[i]);
|
||||
if (!builds[build.shipId]) {
|
||||
builds[build.shipId] = {};
|
||||
}
|
||||
@@ -226,16 +225,17 @@ export default class ModalImport extends TranslatedComponent {
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
_validateImport() {
|
||||
_validateImport(e) {
|
||||
let importData = null;
|
||||
let importString = $scope.importString.trim();
|
||||
let importString = e.target.value;
|
||||
this.setState({
|
||||
builds: null,
|
||||
comparisons: null,
|
||||
discounts: null,
|
||||
errorMsg: null,
|
||||
importValid: false,
|
||||
insurance: null
|
||||
insurance: null,
|
||||
importString,
|
||||
});
|
||||
|
||||
if (!importString) {
|
||||
@@ -246,7 +246,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
if (textBuildRegex.test(importString)) { // E:D Shipyard build text
|
||||
importTextBuild(importString);
|
||||
} else { // JSON Build data
|
||||
importData = angular.fromJson($scope.importString);
|
||||
importData = JSON.parse(importString);
|
||||
|
||||
if (!importData || typeof importData != 'object') {
|
||||
throw 'Must be an object or array!';
|
||||
@@ -272,7 +272,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
let builds = null, comparisons = null;
|
||||
|
||||
if (this.state.builds) {
|
||||
builds = $scope.builds;
|
||||
builds = this.state.builds;
|
||||
for (let shipId in builds) {
|
||||
for (let buildName in builds[shipId]) {
|
||||
let code = builds[shipId][buildName];
|
||||
@@ -286,7 +286,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
|
||||
if (this.state.comparisons) {
|
||||
let comparisons = $scope.comparisons;
|
||||
let comparisons = this.state.comparisons;
|
||||
for (let name in comparisons) {
|
||||
comparisons[name].useName = name;
|
||||
}
|
||||
@@ -341,19 +341,20 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
render() {
|
||||
let translate = this.context.language.translate;
|
||||
let state = this.state;
|
||||
let importStage;
|
||||
|
||||
if (this.state.processed) {
|
||||
if (!state.processed) {
|
||||
importStage = (
|
||||
<div>
|
||||
<textarea className='cb json' onCange={this.validateImport.bind(this)} placeholder={translate('PHRASE_IMPORT') | translate} />
|
||||
<button className='l cap' onClick={this.process.bind(this)} disabled={!this.state.importValid} >{translate('proceed')}</button>
|
||||
<div className='l warning' style={{ marginLeft:'3em' }}>{this.state.errorMsg}</div>
|
||||
<textarea className='cb json' onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
|
||||
<button className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
|
||||
<div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let comparisonTable, edit, buildRows = [];
|
||||
if (this.state.comparisons) {
|
||||
if (state.comparisons) {
|
||||
let comparisonRows = [];
|
||||
|
||||
for (let name in comparisons) {
|
||||
@@ -387,7 +388,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
}
|
||||
|
||||
if(this.state.canEdit) {
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} ng-click={() => this.setState({processed: false})}>{translate('edit data')}</button>
|
||||
edit = <button className='l cap' style={{ marginLeft: '2em' }} onClick={() => this.setState({processed: false})}>{translate('edit data')}</button>
|
||||
}
|
||||
|
||||
let builds = this.state.builds;
|
||||
@@ -410,11 +411,11 @@ export default class ModalImport extends TranslatedComponent {
|
||||
|
||||
importStage = (
|
||||
<div>
|
||||
<table className='l' style='overflow:hidden;margin: 1em 0; width: 100%;'>
|
||||
<table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style='text-align:left' >{translate('ship')}</th>
|
||||
<th style='text-align:left' >{translate('build name')}</th>
|
||||
<th style={{ textAlign: 'left' }} >{translate('ship')}</th>
|
||||
<th style={{ textAlign: 'left' }} >{translate('build name')}</th>
|
||||
<th >{translate('action')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -422,9 +423,7 @@ export default class ModalImport extends TranslatedComponent {
|
||||
{buildRows}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{comparisonTable}
|
||||
|
||||
<button className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
|
||||
{edit}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
@@ -27,7 +24,8 @@ export default class PowerBands extends TranslatedComponent {
|
||||
static propTypes = {
|
||||
bands: React.PropTypes.array.isRequired,
|
||||
available: React.PropTypes.number.isRequired,
|
||||
code: React.PropTypes.string
|
||||
width: React.PropTypes.number.isRequired,
|
||||
code: React.PropTypes.string,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
@@ -37,39 +35,46 @@ export default class PowerBands extends TranslatedComponent {
|
||||
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._updateDimensions = this._updateDimensions.bind(this);
|
||||
this._updateScales = this._updateScales.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: {}
|
||||
};
|
||||
|
||||
if (props.width) {
|
||||
this._updateDimensions(props, context.sizeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
_initSizes(size) {
|
||||
_updateDimensions(props, size) {
|
||||
let barHeight = Math.round(20 * size);
|
||||
let innerHeight = (barHeight * 2) + 2;
|
||||
let mTop = Math.round(25 * size);
|
||||
let mBottom = Math.round(25 * size);
|
||||
let mLeft = Math.round(45 * size);
|
||||
let mRight = Math.round(140 * size);
|
||||
let innerWidth = props.width - mLeft - mRight;
|
||||
|
||||
return {
|
||||
this._updateScales(innerWidth, this.state.maxPwr, props.available);
|
||||
|
||||
this.setState({
|
||||
barHeight,
|
||||
innerHeight,
|
||||
mTop,
|
||||
mBottom,
|
||||
mLeft: Math.round(45 * size),
|
||||
mRight: Math.round(130 * size),
|
||||
mLeft,
|
||||
mRight,
|
||||
innerWidth,
|
||||
height: innerHeight + mTop + mBottom,
|
||||
retY: (barHeight / 2),
|
||||
depY: (barHeight * 1.5) - 1
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
_selectNone() {
|
||||
@@ -102,34 +107,18 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.setState({ dep: Object.assign({}, dep) });
|
||||
}
|
||||
|
||||
_updateAxes(innerWidth, maxPwr, available) {
|
||||
_updateScales(innerWidth, maxPwr, available) {
|
||||
this.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
|
||||
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
|
||||
}
|
||||
|
||||
_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._updateScales(innerWidth, nextMaxPwr, nextProps.available);
|
||||
this.setState({ maxPwr: nextMaxPwr });
|
||||
}
|
||||
|
||||
@@ -137,19 +126,22 @@ export default class PowerBands extends TranslatedComponent {
|
||||
this.wattAxis.tickFormat(nextContext.language.formats.r2);
|
||||
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.resizeListener.remove();
|
||||
this.sizeListener.remove();
|
||||
if (nextProps.width != this.props.width || this.context !== nextContext) {
|
||||
this._updateDimensions(nextProps, nextContext.sizeRatio);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 { available, bands, width } = props;
|
||||
let { innerWidth, maxPwr, ret, dep } = state;
|
||||
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
|
||||
let deployed = [];
|
||||
let retracted = [];
|
||||
@@ -158,71 +150,69 @@ export default class PowerBands extends TranslatedComponent {
|
||||
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;
|
||||
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);
|
||||
if (b.retracted > 0) {
|
||||
let retLbl = bandText(b.retracted, i, wattScale);
|
||||
|
||||
retracted.push(<rect
|
||||
key={'rB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
|
||||
height={sizes.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
|
||||
y={1}
|
||||
retracted.push(<rect
|
||||
key={'rB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
|
||||
height={state.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
|
||||
y={1}
|
||||
onClick={this._selectRet.bind(this, i)}
|
||||
className={getClass(ret[i], b.retractedSum, available)}
|
||||
/>);
|
||||
|
||||
if (retLbl) {
|
||||
retracted.push(<text
|
||||
key={'rT' + i}
|
||||
dy='0.5em'
|
||||
textAnchor='middle'
|
||||
height={state.barHeight}
|
||||
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
|
||||
y={state.retY}
|
||||
onClick={this._selectRet.bind(this, i)}
|
||||
className={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>
|
||||
);
|
||||
}
|
||||
className='primary-bg'>{retLbl}</text>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (outerWidth && (b.retracted > 0 || b.deployed > 0)) {
|
||||
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
|
||||
if (b.retracted > 0 || b.deployed > 0) {
|
||||
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
|
||||
|
||||
deployed.push(<rect
|
||||
key={'dB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
|
||||
height={sizes.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
|
||||
y={sizes.barHeight + 1}
|
||||
deployed.push(<rect
|
||||
key={'dB' + i}
|
||||
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
|
||||
height={state.barHeight}
|
||||
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
|
||||
y={state.barHeight + 1}
|
||||
onClick={this._selectDep.bind(this, i)}
|
||||
className={getClass(dep[i], b.deployedSum, available)}
|
||||
/>);
|
||||
|
||||
if (depLbl) {
|
||||
deployed.push(<text
|
||||
key={'dT' + i}
|
||||
dy='0.5em'
|
||||
textAnchor='middle'
|
||||
height={state.barHeight}
|
||||
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
|
||||
y={state.depY}
|
||||
onClick={this._selectDep.bind(this, i)}
|
||||
className={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>
|
||||
);
|
||||
}
|
||||
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})`}>
|
||||
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
|
||||
<g transform={`translate(${state.mLeft},${state.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>
|
||||
@@ -231,12 +221,12 @@ export default class PowerBands extends TranslatedComponent {
|
||||
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>
|
||||
className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
|
||||
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
|
||||
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text>
|
||||
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end'>{translate('dep')}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum)) + ' (' + pct1(Math.max(0, retSum / available)) + ')'}</text>
|
||||
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import cn from 'classnames';
|
||||
import TranslatedComponent from './TranslatedComponent';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import PowerBands from './PowerBands';
|
||||
import { slotName, nameComparator } from '../utils/SlotFunctions';
|
||||
import { Power, NoPower } from './SvgIcons';
|
||||
@@ -22,61 +24,43 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
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._updateWidth = this._updateWidth.bind(this);
|
||||
this._sort = this._sort.bind(this);
|
||||
|
||||
this.state = {}; // State is initialized through componentWillMount
|
||||
this.state = {
|
||||
predicate: 'n',
|
||||
desc: true,
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
_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');
|
||||
}
|
||||
_sort(ship, predicate, desc) {
|
||||
let powerList = ship.powerList;
|
||||
|
||||
_sortType() {
|
||||
this.props.ship.powerList.sort((a, b) => a.type.localeCompare(b.type));
|
||||
this._sortOrder('t');
|
||||
}
|
||||
switch (predicate) {
|
||||
case 'n': powerList.sort(nameComparator(this.context.language.translate)); break;
|
||||
case 't': powerList.sort((a, b) => a.type.localeCompare(b.type)); break;
|
||||
case 'pri': powerList.sort((a, b) => a.priority - b.priority);break;
|
||||
case 'pwr': powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0)); break;
|
||||
case 'r': powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b)); break;
|
||||
case 'd': powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true)); break;
|
||||
}
|
||||
|
||||
_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');
|
||||
if (!desc) {
|
||||
powerList.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
_priority(slot, inc) {
|
||||
@@ -128,14 +112,30 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
return powerRows;
|
||||
}
|
||||
|
||||
_updateWidth() {
|
||||
this.setState({ width: findDOMNode(this).offsetWidth });
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this._sortName();
|
||||
// Listen to window resize
|
||||
this._sort(this.props.ship, this.state.predicate, this.state.desc);
|
||||
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateWidth);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._updateWidth();
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState) {
|
||||
// Can optimize this later: only sort when
|
||||
// - predicate/desc changes
|
||||
// - modules/language change AND sorting by type, name
|
||||
// - power changes and sorting by pwr
|
||||
// - enabled/disabled changes and sorting by priority
|
||||
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
// remove window listener
|
||||
// remove mouse move listener / touch listner?
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -143,18 +143,19 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
let { translate, formats } = this.context.language;
|
||||
let pwr = formats.f2;
|
||||
let pp = ship.standard[0].m;
|
||||
let sortOrder = this._sortOrder;
|
||||
|
||||
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>
|
||||
<th colSpan='2' className='sortable le' onClick={sortOrder.bind(this, 'n')} >{translate('module')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 't')} >{translate('type')}</th>
|
||||
<th style={{ width: '4em' }} className='sortable' onClick={sortOrder.bind(this, 'pri')} >{translate('pri')}</th>
|
||||
<th colSpan='2' className='sortable' onClick={sortOrder.bind(this, 'pwr')} >{translate('PWR')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'r')} >{translate('ret')}</th>
|
||||
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'd')} >{translate('dep')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -172,7 +173,7 @@ export default class PowerManagement extends TranslatedComponent {
|
||||
{this._renderPowerRows(ship, translate, pwr, formats.pct1)}
|
||||
</tbody>
|
||||
</table>
|
||||
<PowerBands code={code} available={ship.standard[0].m.pGen} bands={ship.priorityBands} />
|
||||
<PowerBands width={this.state.width} code={code} available={ship.standard[0].m.pGen} bands={ship.priorityBands} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import TranslatedComponent from './TranslatedComponent';
|
||||
import cn from 'classnames';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import { Warning } from './SvgIcons';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
|
||||
export default class ShipSummaryTable extends TranslatedComponent {
|
||||
|
||||
@@ -63,8 +62,8 @@ export default class ShipSummaryTable extends TranslatedComponent {
|
||||
<tr>
|
||||
<td className='cap'>{translate(SizeMap[ship.class])}</td>
|
||||
<td>{ship.agility}/10</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u.ms}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u.ms}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
|
||||
<td>{round(ship.totalDps)}</td>
|
||||
<td>{int(ship.armour)} {armourDetails}</td>
|
||||
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>
|
||||
|
||||
68
src/app/components/Slider.jsx
Normal file
68
src/app/components/Slider.jsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
|
||||
export default class Slider extends React.Component {
|
||||
|
||||
static defaultProps = {
|
||||
axis: false,
|
||||
min: 0,
|
||||
max: 1
|
||||
};
|
||||
|
||||
static PropTypes = {
|
||||
axis: React.PropTypes.bool,
|
||||
axisUnit: React.PropTypes.string,
|
||||
min: React.PropTypes.number,
|
||||
max: React.PropTypes.number,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.down = this.down.bind(this);
|
||||
this.up = this.up.bind(this);
|
||||
}
|
||||
|
||||
down(e) {
|
||||
let rect = e.currentTarget.getBoundingClientRect();
|
||||
this.move = this.updatePercent.bind(this, rect.left, rect.width);
|
||||
this.move(e);
|
||||
document.addEventListener("mousemove", this.move);
|
||||
document.addEventListener("mouseup", this.up);
|
||||
}
|
||||
|
||||
up() {
|
||||
document.removeEventListener("mousemove", this.move);
|
||||
document.removeEventListener("mouseup", this.up);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.up();
|
||||
}
|
||||
|
||||
updatePercent(left, width, event) {
|
||||
this.props.onChange(Math.min(Math.max((event.clientX - left) / width, 0), 1));
|
||||
}
|
||||
|
||||
render() {
|
||||
let pctStr = (this.props.percent * 100) + '%';
|
||||
let { axis, axisUnit, min, max } = this.props;
|
||||
let axisGroup;
|
||||
|
||||
if (axis) {
|
||||
axisGroup = <g style={{ fontSize: '.7em' }}>
|
||||
<text className='primary-disabled' y="3em" x="0" style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
|
||||
<text className='primary-disabled' y="3em" x="50%" style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
|
||||
<text className='primary-disabled' y="3em" x="99%" style={{ textAnchor: 'middle' }}>{max + axisUnit}</text>
|
||||
</g>;
|
||||
}
|
||||
|
||||
return <svg style={{ width: '100%', height: axis ? '2.5em' : '1.5em', padding: '0 0.6em', cursor: 'col-resize', boxSizing: 'border-box' }}>
|
||||
<rect className='primary' style={{ opacity: 0.3 }} y='0.25em' rx='0.3em' ry='0.3em' width='100%' height='0.7em' />
|
||||
<rect className='primary-disabled'y='0.45em' rx='0.15em' ry='0.15em' width={pctStr} height='0.3em' />
|
||||
<circle className='primary' r='0.6em' cy='0.6em' cx={pctStr} />
|
||||
<rect width='100%' height='100%' fillOpacity='0' onMouseDown={this.down}/>
|
||||
{axisGroup}
|
||||
</svg>;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export default class StandardSlot extends TranslatedComponent {
|
||||
<div className={'cb'}>
|
||||
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
|
||||
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{units.km}</div> : null }
|
||||
{ m.range ? <div className={'l'}>{translate('range')}: {m.range}{units.km}</div> : null }
|
||||
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
|
||||
{ m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</div> : null }
|
||||
{ m.pGen ? <div className={'l'}>{translate('power')}: {m.pGen}{units.MW}</div> : null }
|
||||
|
||||
@@ -1,21 +1,44 @@
|
||||
import React from 'react';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
|
||||
/**
|
||||
* @class Abstract TranslatedComponent
|
||||
*/
|
||||
export default class TranslatedComponent extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
language: React.PropTypes.object.isRequired
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired
|
||||
}
|
||||
|
||||
/**
|
||||
* Created an instance of a Translated Component. This is an abstract class.
|
||||
* @param {object} props Properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.didContextChange = this.didContextChange.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the context change incldues a language or size change
|
||||
* @param {object} nextContext The incoming / next context
|
||||
* @return {boolean} true if the language has changed
|
||||
*/
|
||||
didContextChange(nextContext){
|
||||
return nextContext.language !== this.context.language;
|
||||
return nextContext.language !== this.context.language || nextContext.sizeRatio != this.context.sizeRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated components are 'pure' components that only render when
|
||||
* props, state, or context changes. This method performs a shallow comparison to
|
||||
* determine change.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {objec} nextState
|
||||
* @param {objec} nextContext
|
||||
* @return {boolean} True if props, state, or context has changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps, nextState, nextContext) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
|| !shallowEqual(this.state, nextState)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
angular.module('app').directive('comparisonTable', ['$state', '$translate', '$rootScope', function($state, $translate, $rootScope) {
|
||||
|
||||
function tblHeader(facets) {
|
||||
var r1 = ['<tr class="main"><th rowspan="2" class="prop" prop="name">', $translate.instant('SHIP'), '</th><th rowspan="2" class="prop" prop="buildName">', $translate.instant('BUILD'), '</th>'];
|
||||
var r2 = [];
|
||||
for (var i = 0, l = facets.length; i < l; i++) {
|
||||
if (facets[i].active) {
|
||||
var f = facets[i];
|
||||
var p = f.props;
|
||||
var pl = p.length;
|
||||
r1.push('<th rowspan="', f.props.length == 1 ? 2 : 1, '" colspan="', pl, '"');
|
||||
|
||||
if (pl == 1) {
|
||||
r1.push(' prop="', p[0], '" class="prop"');
|
||||
} else {
|
||||
for (var j = 0; j < pl; j++) {
|
||||
r2.push('<th prop="', p[j], '" class="prop ', j === 0 ? 'lft' : '', '">', $translate.instant(f.lbls[j]), '</th>');
|
||||
}
|
||||
}
|
||||
|
||||
r1.push('>', $translate.instant(f.title), '</th>');
|
||||
}
|
||||
}
|
||||
r1.push('</tr><tr>');
|
||||
r1.push(r2.join(''));
|
||||
r1.push('</tr>');
|
||||
return r1.join('');
|
||||
}
|
||||
|
||||
function tblBody(facets, builds) {
|
||||
var body = [];
|
||||
|
||||
if (builds.length === 0) {
|
||||
return '<td colspan="100" class="cen">No builds added to comparison!</td';
|
||||
}
|
||||
|
||||
for (var i = 0, l = builds.length; i < l; i++) {
|
||||
var b = builds[i];
|
||||
body.push('<tr class="tr">');
|
||||
var href = $state.href('outfit', { shipId: b.id, code: b.code, bn: b.buildName });
|
||||
body.push('<td class="tl"><a href="', href, '">', b.name, '</a></td>');
|
||||
body.push('<td class="tl"><a href="', href, '">', b.buildName, '</a></td>');
|
||||
|
||||
for (var j = 0, fl = facets.length; j < fl; j++) {
|
||||
if (facets[j].active) {
|
||||
var f = facets[j];
|
||||
var p = f.props;
|
||||
for (var k = 0, pl = p.length; k < pl; k++) {
|
||||
body.push('<td>', $rootScope[f.fmt](b[p[k]]), '<u> ', $translate.instant(f.unit), '</u></td>');
|
||||
}
|
||||
}
|
||||
}
|
||||
body.push('</tr>');
|
||||
}
|
||||
|
||||
return body.join('');
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
|
||||
link: function(scope, element) {
|
||||
var header = angular.element('<thead></thead>');
|
||||
var body = angular.element('<tbody></tbody>');
|
||||
element.append(header);
|
||||
element.append(body);
|
||||
|
||||
var updateAll = function() {
|
||||
header.html(tblHeader(scope.facets));
|
||||
body.html(tblBody(scope.facets, scope.builds));
|
||||
};
|
||||
|
||||
scope.$watchCollection('facets', updateAll);
|
||||
scope.$watch('tblUpdate', updateAll);
|
||||
scope.$watchCollection('builds', function() {
|
||||
body.html(tblBody(scope.facets, scope.builds));
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,247 +0,0 @@
|
||||
angular.module('app').directive('lineChart', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) {
|
||||
|
||||
var RENDER_POINTS = 20; // Only render 20 points on the graph
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
config: '=',
|
||||
series: '='
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var seriesConfig = scope.series,
|
||||
series = seriesConfig.series,
|
||||
color = d3.scale.ordinal().range(scope.series.colors ? scope.series.colors : ['#ff8c0d']),
|
||||
config = scope.config,
|
||||
labels = config.labels,
|
||||
margin = { top: 15, right: 15, bottom: 35, left: 60 },
|
||||
fmtLong = null,
|
||||
func = seriesConfig.func,
|
||||
drag = d3.behavior.drag(),
|
||||
dragging = false,
|
||||
// Define Scales
|
||||
x = d3.scale.linear(),
|
||||
y = d3.scale.linear(),
|
||||
// Define Axes
|
||||
xAxis = d3.svg.axis().scale(x).outerTickSize(0).orient('bottom'),
|
||||
yAxis = d3.svg.axis().scale(y).ticks(6).outerTickSize(0).orient('left'),
|
||||
data = [];
|
||||
|
||||
// Create chart
|
||||
var svg = d3.select(element[0]).append('svg');
|
||||
var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
|
||||
var lines = vis.append('g');
|
||||
|
||||
// Define Area
|
||||
var line = d3.svg.line().y(function(d) { return y(d[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');
|
||||
|
||||
// Create X Axis SVG Elements
|
||||
var xLbl = vis.append('g').attr('class', 'x axis');
|
||||
var xTxt = xLbl.append('text')
|
||||
.attr('class', 'cap')
|
||||
.attr('y', 30)
|
||||
.attr('dy', '.1em')
|
||||
.style('text-anchor', 'middle');
|
||||
|
||||
// xTxt.append('tspan').attr('class', 'metric');
|
||||
// yTxt.append('tspan').attr('class', 'metric');
|
||||
|
||||
// Create and Add tooltip
|
||||
var tipHeight = 2 + (1.25 * (series ? series.length : 0.75));
|
||||
var tips = vis.append('g').style('display', 'none').attr('class', 'tooltip');
|
||||
var markers = vis.append('g').style('display', 'none');
|
||||
|
||||
tips.append('rect')
|
||||
.attr('height', tipHeight + 'em')
|
||||
.attr('y', (-tipHeight / 2) + 'em')
|
||||
.attr('class', 'tip');
|
||||
|
||||
tips.append('text')
|
||||
.attr('class', 'label x')
|
||||
.attr('dy', (-tipHeight / 2) + 'em')
|
||||
.attr('y', '1.25em');
|
||||
|
||||
var background = vis.append('rect') // Background to capture hover/drag
|
||||
.attr('fill-opacity', 0)
|
||||
.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);
|
||||
|
||||
updateFormats();
|
||||
|
||||
function render() {
|
||||
var width = element[0].parentElement.offsetWidth,
|
||||
height = width * 0.5 * $rootScope.sizeRatio,
|
||||
xMax = seriesConfig.xMax,
|
||||
xMin = seriesConfig.xMin,
|
||||
yMax = seriesConfig.yMax,
|
||||
yMin = seriesConfig.yMin,
|
||||
w = width - margin.left - margin.right,
|
||||
h = height - margin.top - margin.bottom,
|
||||
c, s, val, yVal, delta;
|
||||
|
||||
data.length = 0; // Reset Data array
|
||||
|
||||
if (seriesConfig.xMax == seriesConfig.xMin) {
|
||||
line.x(function(d, i) { return i * w; });
|
||||
} else {
|
||||
line.x(function(d) { return x(d[0]); });
|
||||
}
|
||||
|
||||
if (series) {
|
||||
for (s = 0; s < series.length; s++) {
|
||||
data.push([]);
|
||||
}
|
||||
|
||||
if (xMax == xMin) {
|
||||
yVal = func(xMin);
|
||||
for (s = 0; s < series.length; s++) {
|
||||
data[s].push( [ xMin, yVal[ series[s] ] ], [ 1, yVal[ series[s] ] ]);
|
||||
}
|
||||
} else {
|
||||
delta = (xMax - xMin) / RENDER_POINTS;
|
||||
val = 0;
|
||||
for (c = 0; c <= RENDER_POINTS; c++) {
|
||||
yVal = func(val);
|
||||
for (s = 0; s < series.length; s++) {
|
||||
data[s].push([ val, yVal[ series[s] ] ]);
|
||||
}
|
||||
val += delta;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
var seriesData = [];
|
||||
if (xMax == xMin) {
|
||||
yVal = func(xMin);
|
||||
seriesData.push([ xMin, yVal ], [ 1, yVal ]);
|
||||
} else {
|
||||
delta = (xMax - xMin) / RENDER_POINTS;
|
||||
val = 0;
|
||||
for (c = 0; c <= RENDER_POINTS; c++) {
|
||||
seriesData.push([val, func(val) ]);
|
||||
val += delta;
|
||||
}
|
||||
}
|
||||
|
||||
data.push(seriesData);
|
||||
}
|
||||
|
||||
// Update Chart Size
|
||||
svg.attr('width', width).attr('height', height);
|
||||
background.attr('height', h).attr('width', w);
|
||||
|
||||
// Update domain and scale for axes
|
||||
x.range([0, w]).domain([xMin, xMax]).clamp(true);
|
||||
xLbl.attr('transform', 'translate(0,' + h + ')');
|
||||
xTxt.attr('x', w / 2);
|
||||
y.range([h, 0]).domain([yMin, yMax]);
|
||||
yTxt.attr('x', -h / 2);
|
||||
vis.selectAll('.y.axis').call(yAxis);
|
||||
vis.selectAll('.x.axis').call(xAxis);
|
||||
|
||||
lines.selectAll('path.line')
|
||||
.data(data)
|
||||
.attr('d', line) // Update existing series
|
||||
.enter() // Add new series
|
||||
.append('path')
|
||||
.attr('class', 'line')
|
||||
.attr('stroke', function(d, i) { return color(i); })
|
||||
.attr('stroke-width', 2)
|
||||
.attr('d', line);
|
||||
|
||||
tips.selectAll('text.label.y').data(data).enter()
|
||||
.append('text')
|
||||
.attr('class', 'label y')
|
||||
.attr('dy', (-tipHeight / 2) + 'em')
|
||||
.attr('y', function(d, i) { return 1.25 * (i + 2) + 'em'; });
|
||||
|
||||
markers.selectAll('circle.marker').data(data).enter().append('circle').attr('class', 'marker').attr('r', 4);
|
||||
}
|
||||
|
||||
function showTip() {
|
||||
tips.style('display', null);
|
||||
markers.style('display', null);
|
||||
}
|
||||
|
||||
function hideTip() {
|
||||
if (!dragging) {
|
||||
tips.style('display', 'none');
|
||||
markers.style('display', 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function moveTip() {
|
||||
var xPos = d3.mouse(this)[0],
|
||||
x0 = x.invert(xPos),
|
||||
y0 = func(x0),
|
||||
yTotal = 0,
|
||||
flip = (x0 / x.domain()[1] > 0.65),
|
||||
tipWidth = 0,
|
||||
minTransY = (tips.selectAll('rect').node().getBoundingClientRect().height / 2) - margin.top;
|
||||
|
||||
tips.selectAll('text.label.y').text(function(d, i) {
|
||||
var yVal = series ? y0[series[i]] : y0;
|
||||
yTotal += yVal;
|
||||
return (series ? $translate.instant(series[i]) : '') + ' ' + fmtLong(yVal);
|
||||
}).append('tspan').attr('class', 'metric').text(' ' + $translate.instant(labels.yAxis.unit));
|
||||
|
||||
tips.selectAll('text').each(function() {
|
||||
if (this.getBBox().width > tipWidth) {
|
||||
tipWidth = Math.ceil(this.getBBox().width);
|
||||
}
|
||||
});
|
||||
|
||||
tipWidth += 8;
|
||||
markers.selectAll('circle.marker').attr('cx', x(x0)).attr('cy', function(d, i) { return y(series ? y0[series[i]] : y0); });
|
||||
tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start');
|
||||
tips.selectAll('text.label.x').text(fmtLong(x0)).append('tspan').attr('class', 'metric').text(' ' + $translate.instant(labels.xAxis.unit));
|
||||
tips.attr('transform', 'translate(' + x(x0) + ',' + Math.max(minTransY, y(yTotal / (series ? series.length : 1))) + ')');
|
||||
tips.selectAll('rect')
|
||||
.attr('width', tipWidth + 4)
|
||||
.attr('x', flip ? -tipWidth - 12 : 8)
|
||||
.style('text-anchor', flip ? 'end' : 'start');
|
||||
|
||||
}
|
||||
|
||||
function updateFormats() {
|
||||
xTxt.text($translate.instant(labels.xAxis.title)).append('tspan').attr('class', 'metric').text(' (' + $translate.instant(labels.xAxis.unit) + ')');
|
||||
yTxt.text($translate.instant(labels.yAxis.title)).append('tspan').attr('class', 'metric').text(' (' + $translate.instant(labels.yAxis.unit) + ')');
|
||||
fmtLong = $rootScope.localeFormat.numberFormat('.2f');
|
||||
xAxis.tickFormat($rootScope.localeFormat.numberFormat('.2r'));
|
||||
yAxis.tickFormat($rootScope.localeFormat.numberFormat('.3r'));
|
||||
render();
|
||||
}
|
||||
|
||||
angular.element($window).bind('orientationchange resize render', render);
|
||||
scope.$watchCollection('series', render); // Watch for changes in the series data
|
||||
scope.$on('languageChanged', updateFormats);
|
||||
scope.$on('$destroy', function() {
|
||||
angular.element($window).unbind('orientationchange resize render', render);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -1,90 +0,0 @@
|
||||
angular.module('app').directive('slider', ['$window', function($window) {
|
||||
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
min: '=',
|
||||
def: '=',
|
||||
max: '=',
|
||||
unit: '=',
|
||||
change: '&onChange',
|
||||
ignoreResize: '='
|
||||
},
|
||||
link: function(scope, element) {
|
||||
var unit = scope.unit,
|
||||
margin = unit ? { top: -10, right: 145, left: 50 } : { top: 0, right: 10, left: 10 },
|
||||
height = unit ? 40 : 20, // Height is fixed
|
||||
h = height - margin.top,
|
||||
fmt = d3.format('.2f'),
|
||||
pct = d3.format('.1%'),
|
||||
def = scope.def !== undefined ? scope.def : scope.max,
|
||||
val = def,
|
||||
svg = d3.select(element[0]).append('svg'),
|
||||
vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'),
|
||||
xAxisContainer = vis.append('g').attr('class', 'x slider-axis').attr('transform', 'translate(0,' + h / 2 + ')'),
|
||||
x = d3.scale.linear(),
|
||||
xAxis = d3.svg.axis().scale(x).orient('bottom').tickFormat(function(d) { return d + unit; }).tickSize(0).tickPadding(12),
|
||||
slider = vis.append('g').attr('class', 'slider'),
|
||||
filled = slider.append('path').attr('class', 'filled').attr('transform', 'translate(0,' + h / 2 + ')'),
|
||||
brush = d3.svg.brush().x(x).extent([scope.max, scope.max]).on('brush', brushed),
|
||||
handle = slider.append('circle').attr('class', 'handle').attr('r', '0.6em'),
|
||||
lbl = unit ? slider.append('g').append('text').attr('y', h / 2) : null;
|
||||
|
||||
slider.call(brush);
|
||||
slider.select('.background').attr('height', h);
|
||||
handle.attr('transform', 'translate(0,' + h / 2 + ')');
|
||||
|
||||
function render() {
|
||||
var width = element[0].offsetWidth, w = width - margin.left - margin.right;
|
||||
svg.attr('width', width).attr('height', height);
|
||||
x.domain([scope.min || 0, scope.max]).range([0, w]).clamp(true);
|
||||
handle.attr('cx', x(val));
|
||||
if (unit) {
|
||||
xAxisContainer.call(xAxis.tickValues([0, scope.max / 4, scope.max / 2, (3 * scope.max) / 4, scope.max]));
|
||||
lbl.attr('x', w + 20);
|
||||
}
|
||||
slider.call(brush.extent([val, val]));
|
||||
drawBrush();
|
||||
slider.selectAll('.extent,.resize').remove();
|
||||
}
|
||||
|
||||
function brushed() {
|
||||
val = x.invert(d3.mouse(this)[0]);
|
||||
brush.extent([val, val]);
|
||||
scope.change({ val: val });
|
||||
drawBrush();
|
||||
}
|
||||
|
||||
function drawBrush() {
|
||||
if (unit) {
|
||||
lbl.text(fmt(val) + ' ' + unit + ' ' + pct(val / scope.max));
|
||||
}
|
||||
handle.attr('cx', x(val));
|
||||
filled.attr('d', 'M0,0V0H' + x(val) + 'V0');
|
||||
}
|
||||
|
||||
/**
|
||||
* Watch for changes in the max, window size
|
||||
*/
|
||||
scope.$watch('max', function(newMax, oldMax) {
|
||||
val = newMax * (val / oldMax); // Retain percentage filled
|
||||
scope.change({ val: val });
|
||||
render();
|
||||
});
|
||||
|
||||
if (!scope.ignoreResize) {
|
||||
angular.element($window).bind('orientationchange resize', render);
|
||||
}
|
||||
|
||||
scope.$on('reset', function(e, resetVal) {
|
||||
val = resetVal;
|
||||
drawBrush();
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
angular.element($window).unbind('orientationchange resize render', render);
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}]);
|
||||
@@ -9,6 +9,11 @@ import d3 from 'd3';
|
||||
|
||||
let fallbackTerms = EN.terms;
|
||||
|
||||
/**
|
||||
* Get the units, translation and format functions for the specified language
|
||||
* @param {string} langCode ISO Language code
|
||||
* @return {object} Language units, translation and format functions
|
||||
*/
|
||||
export function getLanguage(langCode) {
|
||||
let lang, translate;
|
||||
|
||||
@@ -53,15 +58,20 @@ export function getLanguage(langCode) {
|
||||
Ls: <u>{' ' + translate('Ls')}</u>, // Light Seconds
|
||||
LY: <u>{' ' + translate('LY')}</u>, // Light Years
|
||||
MJ: <u>{' ' + translate('MJ')}</u>, // Mega Joules
|
||||
ms: <u>{' ' + translate('m/s')}</u>, // Meters per second
|
||||
'm/s': <u>{' ' + translate('m/s')}</u>, // Meters per second
|
||||
MW: <u>{' ' + translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
|
||||
ps: <u>{translate('/s')}</u>, // per second
|
||||
pm: <u>{translate('/min')}</u>, // per minute
|
||||
T: <u>{' ' + translate('T')}</u>, // Metric Tons
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The list of available languages
|
||||
* @type {Object}
|
||||
*/
|
||||
export const Languages = {
|
||||
en: 'English',
|
||||
de: 'Deutsch',
|
||||
|
||||
@@ -1,215 +1,344 @@
|
||||
import React from 'react';
|
||||
import Page from './Page';
|
||||
import Ships from '../shipyard/Ships';
|
||||
import Router from '../Router';
|
||||
import cn from 'classnames';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import * as ModuleUtils from '../shipyard/ModuleUtils';
|
||||
import { SizeMap } from '../shipyard/Constants';
|
||||
import Link from '../components/Link';
|
||||
import Serializer from '../shipyard/Serializer';
|
||||
import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import Persist from '../stores/Persist';
|
||||
import { SizeMap, ShipFacets } from '../shipyard/Constants';
|
||||
import ComparisonTable from '../components/ComparisonTable';
|
||||
import ModalCompare from '../components/ModalCompare';
|
||||
import { FloppyDisk, Bin, Download, Embed, Rocket, LinkIcon } from '../components/SvgIcons';
|
||||
|
||||
|
||||
function sortBy(predicate) {
|
||||
return (a, b) => {
|
||||
if (a[predicate] === b[predicate]) {
|
||||
return 0;
|
||||
}
|
||||
if (typeof a[predicate] == 'string') {
|
||||
return a[predicate].toLowerCase() > b[predicate].toLowerCase() ? 1 : -1;
|
||||
}
|
||||
return a[predicate] > b[predicate] ? 1 : -1;
|
||||
};
|
||||
}
|
||||
|
||||
export default class ComparisonPage extends Page {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
title: 'Coriolis - Shipyard',
|
||||
shipPredicate: 'name',
|
||||
shipDesc: false
|
||||
};
|
||||
this.context = context;
|
||||
this.shipSummaries = [];
|
||||
|
||||
for (let s in Ships) {
|
||||
this.shipSummaries.push(this._shipSummary(s, Ships[s]));
|
||||
}
|
||||
this._sortShips = this._sortShips.bind(this);
|
||||
this._buildsSelected = this._buildsSelected.bind(this);
|
||||
this.state = this._initState(props, context);
|
||||
}
|
||||
|
||||
_initState(props, context) {
|
||||
let defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost
|
||||
let params = context.route.params;
|
||||
let code = params.code;
|
||||
let name = params.name ? decodeURIComponent(params.name) : null;
|
||||
let newName = '';
|
||||
let compareMode = !code;
|
||||
let facets = [];
|
||||
let builds = [];
|
||||
let saved = false;
|
||||
let predicate = 'name';
|
||||
let desc = false;
|
||||
let importObj = {};
|
||||
|
||||
if (compareMode) {
|
||||
if (name == 'all') {
|
||||
let allBuilds = Persist.getBuilds();
|
||||
newName = name;
|
||||
for (let shipId in allBuilds) {
|
||||
for (let buildName in allBuilds[shipId]) {
|
||||
builds.push(this._createBuild(shipId, buildName, allBuilds[shipId][buildName]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
let comparisonData = Persist.getComparison(name);
|
||||
if (comparisonData) {
|
||||
defaultFacets = comparisonData.facets;
|
||||
comparisonData.builds.forEach((b) => builds.push(this._createBuild(b.shipId, b.buildName)));
|
||||
saved = true;
|
||||
newName = name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let comparisonData = Serializer.toComparison(code);
|
||||
defaultFacets = comparisonData.f;
|
||||
newName = name = comparisonData.n;
|
||||
predicate = comparisonData.p;
|
||||
desc = comparisonData.d;
|
||||
comparisonData.b.forEach((build) => {
|
||||
builds.push(this._createBuild(build.s, build.n, build.c));
|
||||
if (!importObj[build.s]) {
|
||||
importObj[build.s] = {};
|
||||
}
|
||||
importObj[build.s][build.n] = build.c;
|
||||
});
|
||||
} catch (e) {
|
||||
throw { type: 'bad-comparison', message: e.message, details: e };
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < ShipFacets.length; i++) {
|
||||
facets.push(Object.assign({ index: i }, ShipFacets[i]));
|
||||
}
|
||||
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let fi of defaultFacets) {
|
||||
let facet = facets.splice(fi, 1)[0];
|
||||
facet.active = true;
|
||||
selectedFacets.unshift(facet);
|
||||
}
|
||||
|
||||
facets = selectedFacets.concat(facets);
|
||||
console.log(selectedFacets);
|
||||
builds.sort(sortBy(predicate));
|
||||
|
||||
return {
|
||||
title: 'Coriolis - Compare',
|
||||
predicate,
|
||||
desc,
|
||||
facets,
|
||||
builds,
|
||||
compareMode,
|
||||
code,
|
||||
name,
|
||||
newName,
|
||||
saved,
|
||||
importObj
|
||||
};
|
||||
}
|
||||
|
||||
_createBuild(id, name, code) {
|
||||
code = code ? code : Persist.getBuild(id, name); // Retrieve build code if not passed
|
||||
|
||||
if (!code) { // No build found
|
||||
return;
|
||||
}
|
||||
|
||||
let data = Ships[id]; // Get ship properties
|
||||
let b = new Ship(id, data.properties, data.slots); // Create a new Ship instance
|
||||
b.buildFrom(code); // Populate components from code
|
||||
b.buildName = name;
|
||||
return b;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort ships
|
||||
* @param {object} key Sort predicate
|
||||
*/
|
||||
_sortShips(shipPredicate, shipPredicateIndex) {
|
||||
let shipDesc = this.state.shipDesc;
|
||||
|
||||
if (typeof shipPredicateIndex == 'object') {
|
||||
shipPredicateIndex = undefined;
|
||||
_sortShips(predicate) {
|
||||
let { builds, desc } = this.state;
|
||||
if (this.state.predicate == predicate) {
|
||||
desc = !desc;
|
||||
}
|
||||
|
||||
if (this.state.shipPredicate == shipPredicate && this.state.shipPredicateIndex == shipPredicateIndex) {
|
||||
shipDesc = !shipDesc;
|
||||
builds.sort(sortBy(predicate));
|
||||
|
||||
if (desc) {
|
||||
builds.reverse();
|
||||
}
|
||||
|
||||
this.setState({ shipPredicate, shipDesc, shipPredicateIndex });
|
||||
this.setState({ predicate, desc });
|
||||
};
|
||||
|
||||
_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>
|
||||
<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>
|
||||
<td className={cn({ disabled: !s.hp[4] })}>{s.hp[4]}</td>
|
||||
<td className={cn({ disabled: !s.hp[0] })}>{s.hp[0]}</td>
|
||||
<td className={cn({ disabled: !s.int[0] })}>{s.int[0]}</td>
|
||||
<td className={cn({ disabled: !s.int[1] })}>{s.int[1]}</td>
|
||||
<td className={cn({ disabled: !s.int[2] })}>{s.int[2]}</td>
|
||||
<td className={cn({ disabled: !s.int[3] })}>{s.int[3]}</td>
|
||||
<td className={cn({ disabled: !s.int[4] })}>{s.int[4]}</td>
|
||||
<td className={cn({ disabled: !s.int[5] })}>{s.int[5]}</td>
|
||||
<td className={cn({ disabled: !s.int[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>
|
||||
</tr>;
|
||||
_selectBuilds() {
|
||||
InterfaceEvents.showModal(React.cloneElement(
|
||||
<ModalCompare onSelect={this._buildsSelected}/>,
|
||||
{ builds: this.state.builds }
|
||||
));
|
||||
}
|
||||
|
||||
_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);
|
||||
_buildsSelected(newBuilds) {
|
||||
InterfaceEvents.hideModal();
|
||||
let builds = [];
|
||||
|
||||
for (let b of newBuilds) {
|
||||
builds.push(this._createBuild(b.id, b.buildName));
|
||||
}
|
||||
|
||||
this.setState({ builds });
|
||||
}
|
||||
|
||||
_toggleFacet(facet) {
|
||||
facet.active = !facet.active;
|
||||
this.setState({ facets: [].concat(this.state.facets), saved: false });
|
||||
}
|
||||
|
||||
_facetDrag(e) {
|
||||
this.dragged = e.currentTarget;
|
||||
let placeholder = this.placeholder = document.createElement("li");
|
||||
placeholder.style.width = this.dragged.offsetWidth + 'px';
|
||||
placeholder.className = "facet-placeholder";
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData("text/html", e.currentTarget);
|
||||
}
|
||||
|
||||
_facetDrop(e) {
|
||||
this.dragged.parentNode.removeChild(this.placeholder);
|
||||
let facets = this.state.facets;
|
||||
let frm = Number(this.dragged.dataset.i);
|
||||
let to = Number(this.over.dataset.i);
|
||||
|
||||
if (frm < to) {
|
||||
to--;
|
||||
}
|
||||
if (this.nodeAfter) {
|
||||
to++;
|
||||
}
|
||||
|
||||
facets.splice(to, 0, facets.splice(frm, 1)[0]);
|
||||
this.dragged.style.display = null;
|
||||
this.setState({ facets: [].concat(facets) });
|
||||
}
|
||||
|
||||
_facetDragOver(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if(e.target.className == "facet-placeholder") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.over = e.target;
|
||||
this.dragged.style.display = "none";
|
||||
let relX = e.clientX - this.over.getBoundingClientRect().left;
|
||||
let width = this.over.offsetWidth / 2;
|
||||
let parent = e.target.parentNode;
|
||||
|
||||
if (parent == e.currentTarget) {
|
||||
if(relX > width) {
|
||||
this.nodeAfter = true;
|
||||
parent.insertBefore(this.placeholder, e.target.nextElementSibling);
|
||||
}
|
||||
else {
|
||||
this.nodeAfter = false;
|
||||
parent.insertBefore(this.placeholder, e.target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps, nextState, nextContext) {
|
||||
if (this.context.language !== nextContext.language) {
|
||||
this._renderSummaries(language);
|
||||
_onNameChange(e) {
|
||||
this.setState({ newName: e.target.value, saved: false });
|
||||
}
|
||||
|
||||
_delete() {
|
||||
Persist.deleteComparison(this.state.name);
|
||||
Router.go('/compare');
|
||||
}
|
||||
|
||||
_import() {
|
||||
|
||||
}
|
||||
|
||||
_save() {
|
||||
let { newName, builds, facets } = this.state;
|
||||
|
||||
let selectedFacets = [];
|
||||
facets.forEach((f) => {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
}
|
||||
});
|
||||
console.log(selectedFacets);
|
||||
//Persist.saveComparison(newName, builds, selectedFacets);
|
||||
Router.replace(`/compare/${encodeURIComponent(this.state.newName)}`)
|
||||
this.setState ({ name: newName, saved: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the long permalink URL
|
||||
* @return {string} The long permalink URL
|
||||
*/
|
||||
_genPermalink() {
|
||||
let { facets, builds, name, predicate, desc } = this.state;
|
||||
let selectedFacets = [];
|
||||
|
||||
for (let f of facets){
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
}
|
||||
}
|
||||
|
||||
let code = Serializer.fromComparison(name, builds, selectedFacets, predicate, desc);
|
||||
// send code to permalink modal
|
||||
}
|
||||
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextProps, nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let shipSummaries = this.shipSummaries;
|
||||
let shipPredicate = this.state.shipPredicate;
|
||||
let shipPredicateIndex = this.state.shipPredicateIndex;
|
||||
let shipRows = [];
|
||||
let sortShips = (predicate, index) => this._sortShips.bind(this, predicate, index);
|
||||
|
||||
// Sort shipsOverview
|
||||
shipSummaries.sort((a, b) => {
|
||||
let valA = a[shipPredicate], valB = b[shipPredicate];
|
||||
|
||||
if (shipPredicateIndex != undefined) {
|
||||
valA = valA[shipPredicateIndex];
|
||||
valB = valB[shipPredicateIndex];
|
||||
}
|
||||
|
||||
if (!this.state.shipDesc) {
|
||||
let val = valA;
|
||||
valA = valB;
|
||||
valB = val;
|
||||
}
|
||||
|
||||
if(valA == valB) {
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (valA > valB) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
});
|
||||
|
||||
let formats = this.context.language.formats;
|
||||
let fInt = formats.int;
|
||||
let fRound = formats.round;
|
||||
let translate = this.context.language.translate;
|
||||
let compareHeader;
|
||||
let {newName, name, saved, builds, facets, predicate, desc } = this.state;
|
||||
|
||||
for (let s of shipSummaries) {
|
||||
shipRows.push(s.rowElement);
|
||||
if (this.state.compareMode) {
|
||||
compareHeader = <tr>
|
||||
<td className='head'>{translate('comparison')}</td>
|
||||
<td>
|
||||
<input value={newName} onChange={this._onNameChange} placeholder={translate('Enter Name')} maxLength='50' />
|
||||
<button onClick={this._save} disabled={!newName || newName == 'all' || saved}>
|
||||
<FloppyDisk className='lg'/><span className='button-lbl'>{translate('save')}</span>
|
||||
</button>
|
||||
<button onClick={this._delete} disabled={name == 'all' || !saved}><Bin className='lg warning'/></button>
|
||||
<button onClick={this._selectBuilds}>
|
||||
<Rocket className='lg'/><span className='button-lbl'>{translate('builds')}</span>
|
||||
</button>
|
||||
<button className='r' ng-click='permalink($event)' ng-disabled='builds.length == 0'>
|
||||
<LinkIcon className='lg'/><span className='button-lbl'>{translate('permalink')}</span>
|
||||
</button>
|
||||
<button className='r' ng-click='embed($event)' ng-disabled='builds.length == 0'>
|
||||
<Embed className='lg'/><span className='button-lbl'>{translate('forum')}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>;
|
||||
} else {
|
||||
compareHeader = <tr>
|
||||
<td className='head'>{translate('comparison')}</td>
|
||||
<td>
|
||||
<h3>{name}</h3>
|
||||
<button className='r' onClick={this._import}><Download className='lg'/>{translate('import')}</button>
|
||||
</td>
|
||||
</tr>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'page'}>
|
||||
{/*<table id='comparison'>
|
||||
<tr ng-show='compareMode'>
|
||||
<td class='head' translate='comparison'></td>
|
||||
<td>
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<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>↔ <span ng-bind='f.title | translate'></span></div>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
<div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em'}}>
|
||||
<table id='comparison'>
|
||||
<tbody>
|
||||
{compareHeader}
|
||||
<tr key='facets'>
|
||||
<td className='head'>{translate('compare')}</td>
|
||||
<td>
|
||||
<ul id='facet-container' onDragOver={this._facetDragOver}>
|
||||
{facets.map((f, i) =>
|
||||
<li key={f.title} data-i={i} draggable='true' onDragStart={this._facetDrag} onDragEnd={this._facetDrop} className={cn('facet', {active: f.active})} onClick={this._toggleFacet.bind(this, f)}>
|
||||
{'↔ ' + translate(f.title)}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class='scroll-x'>
|
||||
<table id='comp-tbl' comparison-table ng-click='handleClick($event)'></table>
|
||||
</div>
|
||||
<ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} />
|
||||
|
||||
<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' class='chart' bar-chart facet='f' data='builds'>
|
||||
{/*<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' className='chart' bar-chart facet='f' data='builds'>
|
||||
<h3 ng-click='sort(f.props[0])' >{{f.title | translate}}</h3>
|
||||
</div>
|
||||
</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'>
|
||||
<table>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
<h1>⇆</h1>
|
||||
<table>
|
||||
<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>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
<button class='r dismiss cap' ng-click='selectBuilds(false, $event)' translate='done'></button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { findDOMNode } from 'react-dom';
|
||||
import Page from './Page';
|
||||
import cn from 'classnames';
|
||||
import Router from '../Router';
|
||||
@@ -7,7 +8,7 @@ import InterfaceEvents from '../utils/InterfaceEvents';
|
||||
import { Ships } from 'coriolis-data';
|
||||
import Ship from '../shipyard/Ship';
|
||||
import { toDetailedBuild } from '../shipyard/Serializer';
|
||||
import { FloppyDisk, Bin, Switch, Download, Reload } from '../components/SvgIcons';
|
||||
import { FloppyDisk, Bin, Switch, Download, Reload, Fuel } from '../components/SvgIcons';
|
||||
import ShipSummaryTable from '../components/ShipSummaryTable';
|
||||
import StandardSlotSection from '../components/StandardSlotSection';
|
||||
import HardpointsSlotSection from '../components/HardpointsSlotSection';
|
||||
@@ -17,6 +18,7 @@ import LineChart from '../components/LineChart';
|
||||
import PowerManagement from '../components/PowerManagement';
|
||||
import CostSection from '../components/CostSection';
|
||||
import ModalExport from '../components/ModalExport';
|
||||
import Slider from '../components/Slider';
|
||||
|
||||
const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
|
||||
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
|
||||
@@ -50,6 +52,8 @@ export default class OutfittingPage extends Page {
|
||||
ship.buildWith(data.defaults); // Populate with default components
|
||||
}
|
||||
|
||||
let fuelCapacity = ship.fuelCapacity;
|
||||
|
||||
return {
|
||||
title: 'Outfitting - ' + data.properties.name,
|
||||
costTab: Persist.getCostTab() || 'costs',
|
||||
@@ -57,7 +61,12 @@ export default class OutfittingPage extends Page {
|
||||
shipId,
|
||||
ship,
|
||||
code,
|
||||
savedCode
|
||||
savedCode,
|
||||
fuelCapacity,
|
||||
fuelLevel: 1,
|
||||
jumpRangeChartFunc: ship.getJumpRangeWith.bind(ship, fuelCapacity),
|
||||
totalRangeChartFunc: ship.getFastestRangeWith.bind(ship, fuelCapacity),
|
||||
speedChartFunc: ship.getSpeedsWith.bind(ship, fuelCapacity)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -107,10 +116,15 @@ export default class OutfittingPage extends Page {
|
||||
}
|
||||
|
||||
_shipUpdated() {
|
||||
let { shipId, buildName } = this.state;
|
||||
let newCode = this.state.ship.toString();
|
||||
this._updateRoute(shipId, newCode, buildName);
|
||||
this.setState({ code: newCode });
|
||||
let { shipId, buildName, ship, fuelCapacity } = this.state;
|
||||
let code = ship.toString();
|
||||
|
||||
if (fuelCapacity != ship.fuelCapacity) {
|
||||
this._fuelChange(this.state.fuelLevel);
|
||||
}
|
||||
|
||||
this._updateRoute(shipId, code, buildName);
|
||||
this.setState({ code });
|
||||
}
|
||||
|
||||
_updateRoute(shipId, code, buildName) {
|
||||
@@ -123,16 +137,47 @@ export default class OutfittingPage extends Page {
|
||||
Router.replace(`/outfit/${shipId}/${code}${qStr}`);
|
||||
}
|
||||
|
||||
_fuelChange(fuelLevel) {
|
||||
let ship = this.state.ship;
|
||||
let fuelCapacity = ship.fuelCapacity;
|
||||
let fuel = fuelCapacity * fuelLevel;
|
||||
this.setState({
|
||||
fuelLevel,
|
||||
fuelCapacity,
|
||||
jumpRangeChartFunc: ship.getJumpRangeWith.bind(ship, fuel),
|
||||
totalRangeChartFunc: ship.getFastestRangeWith.bind(ship, fuel),
|
||||
speedChartFunc: ship.getSpeedsWith.bind(ship, fuel)
|
||||
});
|
||||
}
|
||||
|
||||
_updateDimensions() {
|
||||
this.setState({
|
||||
chartWidth: findDOMNode(this.refs.chartThird).offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps, nextContext) {
|
||||
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
|
||||
this.setState(this._initState(nextContext));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateDimensions);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.resizeListener.remove();
|
||||
}
|
||||
|
||||
render() {
|
||||
let { translate, units } = this.context.language;
|
||||
let { translate, units, formats } = this.context.language;
|
||||
let state = this.state;
|
||||
let { ship, code, savedCode, buildName } = state;
|
||||
let { ship, code, savedCode, buildName, chartWidth } = state;
|
||||
let menu = this.props.currentMenu;
|
||||
let shipUpdated = this._shipUpdated;
|
||||
let hStr = ship.getHardpointsString();
|
||||
@@ -140,7 +185,7 @@ export default class OutfittingPage extends Page {
|
||||
let iStr = ship.getInternalString();
|
||||
|
||||
return (
|
||||
<div id='outfit' className={'page'}>
|
||||
<div id='outfit' className={'page'} style={{ fontSize: (this.context.sizeRatio * 0.9) + 'em'}}>
|
||||
<div id='overview'>
|
||||
<h1>{ship.name}</h1>
|
||||
<div id='build'>
|
||||
@@ -164,22 +209,21 @@ export default class OutfittingPage extends Page {
|
||||
</div>
|
||||
|
||||
<ShipSummaryTable ship={ship} code={code} />
|
||||
|
||||
<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} />
|
||||
|
||||
<PowerManagement ship={ship} code={code} onChange={shipUpdated} />
|
||||
<CostSection ship={ship} shipId={state.shipId} buildName={buildName} code={sStr + hStr + iStr} />
|
||||
<CostSection ship={ship} buildName={buildName} code={sStr + hStr + iStr} />
|
||||
|
||||
<div className='group third'>
|
||||
<div ref='chartThird' className='group third'>
|
||||
<h1>{translate('jump range')}</h1>
|
||||
<LineChart
|
||||
width={chartWidth}
|
||||
xMax={ship.cargoCapacity}
|
||||
yMax={ship.unladenRange}
|
||||
xUnit={units.T}
|
||||
yUnit={units.LY}
|
||||
xUnit={translate('T')}
|
||||
yUnit={translate('LY')}
|
||||
yLabel={translate('jump range')}
|
||||
xLabel={translate('cargo')}
|
||||
func={state.jumpRangeChartFunc}
|
||||
@@ -189,10 +233,11 @@ export default class OutfittingPage extends Page {
|
||||
<div className='group third'>
|
||||
<h1>{translate('total range')}</h1>
|
||||
<LineChart
|
||||
width={chartWidth}
|
||||
xMax={ship.cargoCapacity}
|
||||
yMax={ship.boostSpeed}
|
||||
xUnit={units.T}
|
||||
yUnit={units.ms}
|
||||
yMax={ship.unladenTotalRange}
|
||||
xUnit={translate('T')}
|
||||
yUnit={translate('LY')}
|
||||
yLabel={translate('total range')}
|
||||
xLabel={translate('cargo')}
|
||||
func={state.totalRangeChartFunc}
|
||||
@@ -202,25 +247,30 @@ export default class OutfittingPage extends Page {
|
||||
<div className='group third'>
|
||||
<h1>{translate('speed')}</h1>
|
||||
<LineChart
|
||||
width={chartWidth}
|
||||
xMax={ship.cargoCapacity}
|
||||
yMax={ship.boostSpeed}
|
||||
xUnit={units.T}
|
||||
yUnit={units.ms}
|
||||
yMax={ship.topBoost + 10}
|
||||
xUnit={translate('T')}
|
||||
yUnit={translate('m/s')}
|
||||
yLabel={translate('speed')}
|
||||
series={SPEED_SERIES}
|
||||
color={SPEED_COLORS}
|
||||
colors={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 className='group half'>
|
||||
<table style={{ width: '100%', lineHeight: '1em'}}>
|
||||
<tbody >
|
||||
<tr>
|
||||
<td style={{ verticalAlign: 'top', padding:0 }}><Fuel className='xl primary-disabled' /></td>
|
||||
<td><Slider axis={true} onChange={this._fuelChange} axisUnit={translate('T')} percent={state.fuelLevel} max={state.fuelCapacity} /></td>
|
||||
<td className='primary' style={{ width: '10em', verticalAlign: 'top', fontSize: '0.9em' }}>{formats.f2(state.fuelLevel * ship.fuelCapacity)}{units.T} {formats.pct1(state.fuelLevel)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
*/}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
import React from 'react';
|
||||
import shallowEqual from '../utils/shallowEqual';
|
||||
|
||||
/**
|
||||
* @class Abstract Page
|
||||
*/
|
||||
export default class Page extends React.Component {
|
||||
|
||||
static contextTypes = {
|
||||
route: React.PropTypes.object.isRequired,
|
||||
language: React.PropTypes.object.isRequired
|
||||
language: React.PropTypes.object.isRequired,
|
||||
sizeRatio: React.PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
currentMenu: React.PropTypes.any
|
||||
};
|
||||
|
||||
/**
|
||||
* Created an instance of a Page. This is an abstract class.
|
||||
* @param {object} props Properties
|
||||
*/
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -23,16 +31,34 @@ export default class Page extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Translated components are 'pure' components that only render when
|
||||
* props, state, or context changes. This method performs a shallow comparison to
|
||||
* determine change.
|
||||
*
|
||||
* @param {object} nextProps
|
||||
* @param {objec} nextState
|
||||
* @param {objec} nextContext
|
||||
* @return {boolean} True if props, state, or context has changed
|
||||
*/
|
||||
shouldComponentUpdate(nextProps, nextState, nextContext) {
|
||||
return !shallowEqual(this.props, nextProps)
|
||||
|| !shallowEqual(this.state, nextState)
|
||||
|| !shallowEqual(this.context, nextContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the window title upon mount
|
||||
*/
|
||||
componentWillMount() {
|
||||
document.title = this.state.title || 'Coriolis';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the title upon change
|
||||
* @param {Object} newProps Incoming properties
|
||||
* @param {Object} newState Incoming state
|
||||
*/
|
||||
componentWillUpdate(newProps, newState) {
|
||||
document.title = newState.title || 'Coriolis';
|
||||
}
|
||||
|
||||
@@ -91,12 +91,13 @@ export default class ShipyardPage extends Page {
|
||||
<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>{s.agility}</td>
|
||||
<td className='ri'>{fInt(s.speed)}{u['m/s']}</td>
|
||||
<td className='ri'>{fInt(s.boost)}{u['m/s']}</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'>{fInt(s.topSpeed)}{u['m/s']}</td>
|
||||
<td className='ri'>{fInt(s.topBoost)}{u['m/s']}</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>
|
||||
@@ -169,6 +170,7 @@ export default class ShipyardPage extends Page {
|
||||
<th rowSpan={2} className='sortable le' onClick={sortShips('name')}>{translate('ship')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
|
||||
<th rowSpan={2} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
|
||||
<th colSpan={4}>{translate('base')}</th>
|
||||
<th colSpan={4}>{translate('max')}</th>
|
||||
<th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th>
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
angular.module('app').controller('ComparisonController', ['lodash', '$rootScope', '$filter', '$scope', '$state', '$stateParams', '$translate', 'Utils', 'ShipFacets', 'ShipsDB', 'Ship', 'Persist', 'Serializer', function(_, $rootScope, $filter, $scope, $state, $stateParams, $translate, Utils, ShipFacets, Ships, Ship, Persist, Serializer) {
|
||||
$rootScope.title = 'Coriolis - Compare';
|
||||
$scope.predicate = 'name'; // Sort by ship name as default
|
||||
$scope.desc = false;
|
||||
$scope.facetSortOpts = { containment: '#facet-container', orderChanged: function() { $scope.saved = false; } };
|
||||
$scope.builds = [];
|
||||
$scope.unusedBuilds = [];
|
||||
$scope.name = $stateParams.name;
|
||||
$scope.compareMode = !$stateParams.code;
|
||||
$scope.importObj = {}; // Used for importing comparison builds (from permalinked comparison)
|
||||
var defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost
|
||||
var facets = $scope.facets = angular.copy(ShipFacets);
|
||||
var shipId, buildName, comparisonData;
|
||||
|
||||
/**
|
||||
* Add an existing build to the comparison. The build must be saved locally.
|
||||
* @param {string} id The unique ship key/id
|
||||
* @param {string} name The build name
|
||||
*/
|
||||
$scope.addBuild = function(id, name, code) {
|
||||
var data = Ships[id]; // Get ship properties
|
||||
code = code ? code : Persist.builds[id][name]; // Retrieve build code if not passed
|
||||
|
||||
if (!code) { // No build found
|
||||
return;
|
||||
}
|
||||
|
||||
var b = new Ship(id, data.properties, data.slots); // Create a new Ship instance
|
||||
Serializer.toShip(b, code); // Populate components from code
|
||||
// Extend ship instance and add properties below
|
||||
b.buildName = name;
|
||||
b.code = code;
|
||||
b.pctRetracted = b.powerRetracted / b.powerAvailable;
|
||||
b.pctDeployed = b.powerDeployed / b.powerAvailable;
|
||||
$scope.builds.push(b); // Add ship build to comparison
|
||||
$scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc); // Resort
|
||||
_.remove($scope.unusedBuilds, function(o) { // Remove from unused builds
|
||||
return o.id == id && o.buildName == name;
|
||||
});
|
||||
$scope.saved = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes a build from the comparison
|
||||
* @param {string} id The unique ship key/id
|
||||
* @param {string} name The build name
|
||||
*/
|
||||
$scope.removeBuild = function(id, name) {
|
||||
_.remove($scope.builds, function(s) {
|
||||
if (s.id == id && s.buildName == name) {
|
||||
$scope.unusedBuilds.push({ id: id, buildName: name, name: s.name }); // Add build back to unused builds
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
$scope.saved = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles the selected the set of facets used in the comparison
|
||||
* @param {number} i The index of the facet in facets
|
||||
*/
|
||||
$scope.toggleFacet = function(i) {
|
||||
facets[i].active = !facets[i].active;
|
||||
$scope.tblUpdate = !$scope.tblUpdate; // Simple switch to trigger the table to update
|
||||
$scope.saved = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Click handler for sorting by facets in the table
|
||||
* @param {object} e Event object
|
||||
*/
|
||||
$scope.handleClick = function(e) {
|
||||
var elem = angular.element(e.target);
|
||||
if (elem.attr('prop')) { // Get component ID
|
||||
$scope.sort(elem.attr('prop'));
|
||||
|
||||
} else if (elem.attr('del')) { // Delete index
|
||||
$scope.removeBuild(elem.attr('del'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sort the comparison array based on the selected facet / ship property
|
||||
* @param {string} key Ship property
|
||||
*/
|
||||
$scope.sort = function(key) {
|
||||
$scope.desc = $scope.predicate == key ? !$scope.desc : $scope.desc;
|
||||
$scope.predicate = key;
|
||||
$scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc);
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves the current comparison's selected facets and builds
|
||||
*/
|
||||
$scope.save = function() {
|
||||
$scope.name = $scope.name.trim();
|
||||
if ($scope.name == 'all') {
|
||||
return;
|
||||
}
|
||||
var selectedFacets = [];
|
||||
facets.forEach(function(f) {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
}
|
||||
});
|
||||
Persist.saveComparison($scope.name, $scope.builds, selectedFacets);
|
||||
$state.go('compare', { name: $scope.name }, { location: 'replace', notify: false });
|
||||
$scope.saved = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Permantently delete the current comparison
|
||||
*/
|
||||
$scope.delete = function() {
|
||||
Persist.deleteComparison($scope.name);
|
||||
$state.go('compare', { name: null }, { location: 'replace', reload: true });
|
||||
};
|
||||
|
||||
/**
|
||||
* Set saved to false when the name of the comparison is changed.
|
||||
*/
|
||||
$scope.nameChange = function() {
|
||||
$scope.saved = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide/Show the select builds menu
|
||||
* @param {boolean} s Show true/false
|
||||
* @param {Event} e Event Object
|
||||
*/
|
||||
$scope.selectBuilds = function(s, e) {
|
||||
e.stopPropagation();
|
||||
$scope.showBuilds = s;
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the permalink modal
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
$scope.permalink = function(e) {
|
||||
e.stopPropagation();
|
||||
$state.go('modal.link', { url: genPermalink() });
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the forum embed code for the comparison
|
||||
* and show the export modal.
|
||||
*
|
||||
* @param {Event} e Event object
|
||||
*/
|
||||
$scope.embed = function(e) {
|
||||
e.stopPropagation();
|
||||
// Make a request to goo.gl to shorten the URL, returns a promise
|
||||
var promise = Utils.shortenUrl( genPermalink()).then(
|
||||
function(shortUrl) {
|
||||
return Utils.comparisonBBCode(facets, $scope.builds, shortUrl);
|
||||
},
|
||||
function(err) {
|
||||
return 'Error - ' + err.statusText;
|
||||
}
|
||||
);
|
||||
$state.go('modal.export', { promise: promise, title: $translate.instant('FORUM') + ' BBCode' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Generates the long permalink URL
|
||||
* @return {string} The long permalink URL
|
||||
*/
|
||||
function genPermalink() {
|
||||
var selectedFacets = [];
|
||||
facets.forEach(function(f) {
|
||||
if (f.active) {
|
||||
selectedFacets.unshift(f.index);
|
||||
}
|
||||
});
|
||||
var code = Serializer.fromComparison(
|
||||
$scope.name,
|
||||
$scope.builds,
|
||||
selectedFacets,
|
||||
$scope.predicate,
|
||||
$scope.desc
|
||||
);
|
||||
return $state.href('comparison', { code: code }, { absolute: true });
|
||||
}
|
||||
|
||||
/* Event listeners */
|
||||
$scope.$on('close', function() {
|
||||
$scope.showBuilds = false;
|
||||
});
|
||||
|
||||
$scope.$on('languageChanged', function() {
|
||||
$scope.tblUpdate = !$scope.tblUpdate; // Simple switch to trigger the table to update
|
||||
});
|
||||
|
||||
/* Initialization */
|
||||
if ($scope.compareMode) {
|
||||
if ($scope.name == 'all') {
|
||||
for (shipId in Persist.builds) {
|
||||
for (buildName in Persist.builds[shipId]) {
|
||||
$scope.addBuild(shipId, buildName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (shipId in Persist.builds) {
|
||||
for (buildName in Persist.builds[shipId]) {
|
||||
$scope.unusedBuilds.push({ id: shipId, buildName: buildName, name: Ships[shipId].properties.name });
|
||||
}
|
||||
}
|
||||
comparisonData = Persist.getComparison($scope.name);
|
||||
if (comparisonData) {
|
||||
defaultFacets = comparisonData.facets;
|
||||
comparisonData.builds.forEach(function(b) {
|
||||
$scope.addBuild(b.shipId, b.buildName);
|
||||
});
|
||||
$scope.saved = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
comparisonData = Serializer.toComparison($stateParams.code);
|
||||
defaultFacets = comparisonData.f;
|
||||
$scope.name = comparisonData.n;
|
||||
$scope.predicate = comparisonData.p;
|
||||
$scope.desc = comparisonData.d;
|
||||
comparisonData.b.forEach(function(build) {
|
||||
$scope.addBuild(build.s, build.n, build.c);
|
||||
if (!$scope.importObj[build.s]) {
|
||||
$scope.importObj[build.s] = {};
|
||||
}
|
||||
$scope.importObj[build.s][build.n] = build.c;
|
||||
});
|
||||
} catch (e) {
|
||||
throw { type: 'bad-comparison', message: e.message, details: e };
|
||||
}
|
||||
}
|
||||
// Replace fmt with actual format function as defined in rootScope and retain original index
|
||||
facets.forEach(function(f, i) { f.index = i; });
|
||||
// Remove default facets, mark as active, and add them back in selected order
|
||||
_.pullAt(facets, defaultFacets).forEach(function(f) { f.active = true; facets.unshift(f); });
|
||||
$scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc);
|
||||
|
||||
}]);
|
||||
@@ -94,80 +94,78 @@ export const ShipFacets = [
|
||||
{ // 0
|
||||
title: 'agility',
|
||||
props: ['agility'],
|
||||
unit: '',
|
||||
fmt: 'fCrd'
|
||||
fmt: 'int'
|
||||
},
|
||||
{ // 1
|
||||
title: 'speed',
|
||||
props: ['topSpeed', 'topBoost'],
|
||||
lbls: ['thrusters', 'boost'],
|
||||
unit: 'm/s',
|
||||
fmt: 'fCrd'
|
||||
fmt: 'int'
|
||||
},
|
||||
{ // 2
|
||||
title: 'armour',
|
||||
props: ['armour'],
|
||||
unit: '',
|
||||
fmt: 'fCrd'
|
||||
fmt: 'int'
|
||||
},
|
||||
{ // 3
|
||||
title: 'shields',
|
||||
props: ['shieldStrength'],
|
||||
unit: 'MJ',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
},
|
||||
{ // 4
|
||||
title: 'jump range',
|
||||
props: ['unladenRange', 'fullTankRange', 'ladenRange'],
|
||||
lbls: ['max', 'full tank', 'laden'],
|
||||
unit: 'LY',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
},
|
||||
{ // 5
|
||||
title: 'mass',
|
||||
props: ['unladenMass', 'ladenMass'],
|
||||
lbls: ['unladen', 'laden'],
|
||||
unit: 'T',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
},
|
||||
{ // 6
|
||||
title: 'cargo',
|
||||
props: ['cargoCapacity'],
|
||||
unit: 'T',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
},
|
||||
{ // 7
|
||||
title: 'fuel',
|
||||
props: ['fuelCapacity'],
|
||||
unit: 'T',
|
||||
fmt: 'fRound'
|
||||
fmt: 'int'
|
||||
},
|
||||
{ // 8
|
||||
title: 'power',
|
||||
props: ['powerRetracted', 'powerDeployed', 'powerAvailable'],
|
||||
lbls: ['retracted', 'deployed', 'available'],
|
||||
unit: 'MW',
|
||||
fmt: 'fPwr'
|
||||
fmt: 'f2'
|
||||
},
|
||||
{ // 9
|
||||
title: 'cost',
|
||||
props: ['totalCost'],
|
||||
unit: 'CR',
|
||||
fmt: 'fCrd'
|
||||
fmt: 'int'
|
||||
},
|
||||
{ // 10
|
||||
title: 'total range',
|
||||
props: ['unladenTotalRange', 'ladenTotalRange'],
|
||||
lbls: ['unladen', 'laden'],
|
||||
unit: 'LY',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
},
|
||||
{ // 11
|
||||
title: 'DPS',
|
||||
props: ['totalDps'],
|
||||
lbls: ['DPS'],
|
||||
unit: '',
|
||||
fmt: 'fRound'
|
||||
fmt: 'round'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -185,8 +183,10 @@ export const Insurance = {
|
||||
*/
|
||||
export const Discounts = {
|
||||
'0%': 1,
|
||||
'2.5%': 0.975,
|
||||
'5%': 0.95,
|
||||
'10%': 0.90,
|
||||
'12.5%': 0.875,
|
||||
'15%': 0.85,
|
||||
'20%': 0.80,
|
||||
'25%': 0.75
|
||||
|
||||
@@ -165,7 +165,7 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
|
||||
*/
|
||||
export function bulkheads(shipId, index) {
|
||||
let bulkhead = Ships[shipId].bulkheads[index];
|
||||
bulkhead.class = 8;
|
||||
bulkhead.class = 1;
|
||||
bulkhead.rating = 'I';
|
||||
bulkhead.name = BulkheadNames[index]
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export function toDetailedBuild(buildName, ship, code) {
|
||||
internal = ship.internal;
|
||||
|
||||
var data = {
|
||||
$schema: 'http://cdn.coriolis.io/schemas/ship-loadout/2.json#',
|
||||
$schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#',
|
||||
name: buildName,
|
||||
ship: ship.name,
|
||||
references: [{
|
||||
@@ -76,7 +76,7 @@ export function toDetailedBuild(buildName, ship, code) {
|
||||
};
|
||||
|
||||
export function fromDetailedBuild(detailedBuild) {
|
||||
var shipId = _.findKey(ShipsDB, { properties: { name: detailedBuild.ship } });
|
||||
var shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase());
|
||||
|
||||
if (!shipId) {
|
||||
throw 'No such ship: ' + detailedBuild.ship;
|
||||
@@ -133,9 +133,9 @@ export function toDetailedExport(builds) {
|
||||
for (var shipId in builds) {
|
||||
for (var buildName in builds[shipId]) {
|
||||
var code = builds[shipId][buildName];
|
||||
var shipData = ShipsDB[shipId];
|
||||
var shipData = Ships[shipId];
|
||||
var ship = new Ship(shipId, shipData.properties, shipData.slots);
|
||||
toShip(ship, code);
|
||||
ship.buildFrom(code);
|
||||
data.push(toDetailedBuild(buildName, ship, code));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,8 +149,16 @@ export default class Ship {
|
||||
* @param {number} fuel Fuel available in tons
|
||||
* @return {number} Jump range in Light Years
|
||||
*/
|
||||
getJumpRangeForMass(mass, fuel) {
|
||||
return Calc.jumpRange(mass, this.standard[2].m, fuel);
|
||||
getJumpRangeWith(fuel, cargo) {
|
||||
return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
getFastestRangeWith(fuel, cargo) {
|
||||
return Calc.totalRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
|
||||
}
|
||||
|
||||
getSpeedsWith(fuel, cargo) {
|
||||
return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.boost, this.standard[1].m, this.pipSpeed);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,9 +88,7 @@ class Persist extends EventEmitter {
|
||||
let newBuild = !this.builds[shipId][name];
|
||||
this.builds[shipId][name] = code;
|
||||
_put(LS_KEY_BUILDS, this.builds);
|
||||
if (newBuild) {
|
||||
this.emit('buildSaved', shipId, name, code);
|
||||
}
|
||||
this.emit('buildSaved', shipId, name, code);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,7 +230,7 @@ class Persist extends EventEmitter {
|
||||
data[LS_KEY_BUILDS] = this.getBuilds();
|
||||
data[LS_KEY_COMPARISONS] = this.getComparisons();
|
||||
data[LS_KEY_INSURANCE] = this.getInsurance();
|
||||
data[LS_KEY_DISCOUNTS] = this.getDiscount();
|
||||
data[LS_KEY_DISCOUNTS] = this.discounts;
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
|
||||
/**
|
||||
* [slotName description]
|
||||
* @param {[type]} translate [description]
|
||||
* @param {[type]} slot [description]
|
||||
* @return {[type]} [description]
|
||||
* Returns the translate name for the module mounted in the specified
|
||||
* slot.
|
||||
* @param {function} translate Translation function
|
||||
* @param {object} slot Slot object
|
||||
* @return {string} The translated name
|
||||
*/
|
||||
export function slotName(translate, slot) {
|
||||
return slot.m ? translate(slot.m.name || slot.m.grp) : '';
|
||||
@@ -15,5 +16,22 @@ export function slotName(translate, slot) {
|
||||
* @return {function} Comparator function for slot names
|
||||
*/
|
||||
export function nameComparator(translate) {
|
||||
return (a, b) => slotName(translate, a).toLowerCase().localeCompare(slotName(translate, b).toLowerCase());
|
||||
return (a, b) => {
|
||||
a = a.m;
|
||||
b = b.m;
|
||||
|
||||
if (a && !b) {
|
||||
return 1;
|
||||
} else if (!a && b) {
|
||||
return -1;
|
||||
} else if (!a && !b) {
|
||||
return 0;
|
||||
} else if (a.name === b.name && a.grp === b.grp) {
|
||||
if(a.class == b.class) {
|
||||
return a.rating > b.rating ? 1 : -1;
|
||||
}
|
||||
return a.class - b.class;
|
||||
}
|
||||
return translate(a.name || a.grp).localeCompare(translate(b.name || b.grp));
|
||||
};
|
||||
}
|
||||
@@ -13,7 +13,6 @@
|
||||
@import 'select';
|
||||
@import 'modal';
|
||||
@import 'charts';
|
||||
@import 'slider';
|
||||
@import 'chart-tooltip';
|
||||
@import 'buttons';
|
||||
@import 'error';
|
||||
@@ -43,7 +42,7 @@ div, a, li {
|
||||
|
||||
#coriolis {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-height: 95%;
|
||||
}
|
||||
|
||||
.page {
|
||||
|
||||
@@ -54,9 +54,10 @@
|
||||
display: block;
|
||||
.user-select-none();
|
||||
cursor: default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding: 0 1px;
|
||||
|
||||
.as-sortable-placeholder {
|
||||
background-color: @primary-bg;
|
||||
@@ -66,23 +67,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.facet-placeholder {
|
||||
display: inline-block;
|
||||
padding: 0.5em 0.5em;
|
||||
background-color: @primary-bg;
|
||||
}
|
||||
|
||||
.facet {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
background-color: @primary-bg;
|
||||
margin: 0;
|
||||
padding: 0.5em 0.5em;
|
||||
padding: 0 0.5em;
|
||||
line-height: 2.5em;
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
color: @disabled;
|
||||
|
||||
svg {
|
||||
fill: @disabled;
|
||||
}
|
||||
|
||||
.move {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: @warning;
|
||||
svg {
|
||||
|
||||
@@ -114,12 +114,6 @@
|
||||
});
|
||||
}
|
||||
|
||||
.sortable {
|
||||
&:hover {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
.shorten {
|
||||
overflow: hidden;
|
||||
max-width: 8em;
|
||||
@@ -225,15 +219,13 @@
|
||||
|
||||
}
|
||||
|
||||
svg g {
|
||||
.threshold {
|
||||
stroke: @secondary-disabled !important;
|
||||
fill: @secondary-disabled !important;
|
||||
.threshold {
|
||||
stroke: @secondary-disabled !important;
|
||||
fill: @secondary-disabled !important;
|
||||
|
||||
&.exceeded {
|
||||
stroke: @warning !important;
|
||||
fill: @warning !important;
|
||||
}
|
||||
&.exceeded {
|
||||
stroke: @warning !important;
|
||||
fill: @warning !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
|
||||
.slider-axis {
|
||||
line, path {
|
||||
fill: none;
|
||||
stroke: @primary-disabled;
|
||||
}
|
||||
|
||||
text {
|
||||
font-size: 0.7em;
|
||||
fill: @primary-disabled;
|
||||
}
|
||||
|
||||
|
||||
.domain {
|
||||
fill: none;
|
||||
stroke: @primary;
|
||||
stroke-opacity: .3;
|
||||
stroke-width: 0.7em;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.slider {
|
||||
|
||||
text {
|
||||
dominant-baseline: central;
|
||||
fill: @primary;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.filled {
|
||||
stroke-width: 0.3em;
|
||||
stroke-linecap: round;
|
||||
stroke: @primary-disabled;
|
||||
}
|
||||
|
||||
.handle {
|
||||
fill: @primary;
|
||||
stroke-opacity: .5;
|
||||
cursor: crosshair;
|
||||
}
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid @bgBlack;
|
||||
/*required for proper track sizing in FF*/
|
||||
width: 300px;
|
||||
|
||||
&::-moz-range-track, &::-webkit-slider-runnable-track {
|
||||
width: 300px;
|
||||
height: 5px;
|
||||
background: @primary;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-moz-range-thumb, &::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
height: 1em;
|
||||
width: 1em;
|
||||
border-radius: 50%;
|
||||
background: @primary;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
/*hide the outline behind the border*/
|
||||
&:-moz-focusring{
|
||||
outline: 1px solid @bgBlack;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
width: 300px;
|
||||
height: 5px;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
border-width: 6px 0;
|
||||
color: transparent;
|
||||
}
|
||||
&::-ms-fill-lower {
|
||||
background: @primary;
|
||||
border-radius: 10px;
|
||||
}
|
||||
&::-ms-fill-upper {
|
||||
background: @primary;
|
||||
border-radius: 10px;
|
||||
}
|
||||
&::-ms-thumb {
|
||||
border: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: goldenrod;
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,8 @@
|
||||
.sortable {
|
||||
.user-select-none();
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.as-sortable-item, .as-sortable-placeholder {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.as-sortable-item {
|
||||
-ms-touch-action: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.as-sortable-item-handle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.as-sortable-drag {
|
||||
margin: 0;
|
||||
padding:0;
|
||||
opacity: .8;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.as-sortable-hidden {
|
||||
display: none !important;
|
||||
&:hover {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,6 @@ thead {
|
||||
font-weight: normal;
|
||||
padding: 2px 0.4em 0; // Padding top for font vertical alignment
|
||||
|
||||
&.prop {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
color: @primary;
|
||||
}
|
||||
}
|
||||
|
||||
&.lft {
|
||||
border-left: 1px solid @primary-bg;
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"standardRatings": { "enum": ["A", "B", "C", "D", "E"] },
|
||||
"allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "I" ] }
|
||||
"standardRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H"] },
|
||||
"allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H", "I" ] }
|
||||
}
|
||||
}
|
||||
|
||||
308
src/schemas/ship-loadout/3.json
Normal file
308
src/schemas/ship-loadout/3.json
Normal file
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"id": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#",
|
||||
"title": "Ship Loadout",
|
||||
"type": "object",
|
||||
"description": "The details for a specific ship build/loadout",
|
||||
"required": ["name", "ship", "components"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name of the build/loadout",
|
||||
"type": "string",
|
||||
"minLength": 2
|
||||
},
|
||||
"ship": {
|
||||
"description": "The full display name of the ship",
|
||||
"type": "string",
|
||||
"minimum": 3
|
||||
},
|
||||
"manufacturer": {
|
||||
"description": "The ship manufacturer",
|
||||
"type": "string"
|
||||
},
|
||||
"references" : {
|
||||
"description": "3rd Party references and/or links to this build/loadout",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["name","url"],
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"name": {
|
||||
"description": "The name of the 3rd party, .e.g 'Coriolis.io' or 'E:D Shipyard'",
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"description": "The link/url to the 3rd party referencing this build/loadout",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"description": "The components used by this build",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["standard", "internal", "hardpoints", "utility"],
|
||||
"properties": {
|
||||
"standard": {
|
||||
"description": "The set of standard components across all ships",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["bulkheads", "powerPlant", "thrusters", "frameShiftDrive", "lifeSupport", "powerDistributor", "sensors", "fuelTank", "cargoHatch"],
|
||||
"properties": {
|
||||
"bulkheads": {
|
||||
"enum": ["Lightweight Alloy", "Reinforced Alloy", "Military Grade Composite", "Mirrored Surface Composite", "Reactive Surface Composite"]
|
||||
},
|
||||
"cargoHatch": {
|
||||
"required": ["enabled", "priority"],
|
||||
"properties": {
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"powerPlant": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 2, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"thrusters": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 2, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"frameShiftDrive": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 2, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"lifeSupport": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 6 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"powerDistributor": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"sensors": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
},
|
||||
"fuelTank": {
|
||||
"required": ["class", "rating", "enabled", "priority"],
|
||||
"properties": {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 6 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"internal": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["object", "null"],
|
||||
"required": ["class", "rating", "enabled", "priority", "group"],
|
||||
"properties" : {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 8 },
|
||||
"rating": { "$ref": "#/definitions/standardRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 },
|
||||
"group": {
|
||||
"description": "The group of the component, e.g. 'Shield Generator', or 'Cargo Rack'",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name identifying the component (if applicable), e.g. 'Advance Discovery Scanner', or 'Detailed Surface Scanner'",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 3
|
||||
},
|
||||
"hardpoints": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["object", "null"],
|
||||
"required": ["class", "rating", "enabled", "priority", "group", "mount"],
|
||||
"properties" : {
|
||||
"class": { "type": "integer", "minimum": 1, "maximum": 4 },
|
||||
"rating": { "$ref": "#/definitions/allRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 },
|
||||
"mount": { "type": "string", "enum": ["Fixed", "Gimballed", "Turret"] },
|
||||
"group": {
|
||||
"description": "The group of the component, e.g. 'Beam Laser', or 'Missile Rack'",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name identifing the component (if applicable), e.g. 'Retributor', or 'Mining Lance'",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1
|
||||
},
|
||||
"utility": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": ["object", "null"],
|
||||
"required": ["class", "rating", "enabled", "priority", "group"],
|
||||
"properties" : {
|
||||
"class": { "type": "integer", "minimum": 0, "maximum": 0 },
|
||||
"rating": { "$ref": "#/definitions/allRatings" },
|
||||
"enabled": { "type": "boolean" },
|
||||
"priority": { "type": "integer", "minimum": 1, "maximum": 5 },
|
||||
"group": {
|
||||
"description": "The group of the component, e.g. 'Shield Booster', or 'Kill Warrant Scanner'",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"description": "The name identifing the component (if applicable), e.g. 'Point Defence', or 'Electronic Countermeasure'",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats": {
|
||||
"description": "Optional statistics from the build",
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"agility": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"armour": {
|
||||
"description": "Sum of base armour + any hull reinforcements",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"armourAdded":{
|
||||
"description": "Armour added through Hull reinforcement",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"baseShieldStrength": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"baseArmour": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"boost": {
|
||||
"description": "Maximum boost speed of the ships (4 pips, straight-line)",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"cargoCapacity": {
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"class": {
|
||||
"description": "Ship Class/Size [Small, Medium, Large]",
|
||||
"enum": [1,2,3]
|
||||
},
|
||||
"dps": {
|
||||
"description": "Cumulative DPS based on the in-game 1-10 statistic",
|
||||
"type": "integer",
|
||||
"minimum": 0
|
||||
},
|
||||
"hullCost": {
|
||||
"description": "Cost of the ship's hull",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"hullMass": {
|
||||
"description": "Mass of the Ship hull only",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"fuelCapacity": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"fullTankRange": {
|
||||
"description": "Single Jump range with a full tank (unladenMass + fuel)",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"ladenMass": {
|
||||
"description": "Mass of the Ship + fuel + cargo (hull + all components + fuel tank + cargo capacity)",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"ladenRange": {
|
||||
"description": "Single Jump range with full cargo load, see ladenMass",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"masslock": {
|
||||
"description": "Mass Lock Factor of the Ship",
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"shieldStrength": {
|
||||
"description": "Shield strengh in Mega Joules (Mj)",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"speed": {
|
||||
"description": "Maximum speed of the ships (4 pips, straight-line)",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
},
|
||||
"totalCost": {
|
||||
"type": "integer",
|
||||
"minimum": 1
|
||||
},
|
||||
"unladenRange": {
|
||||
"description": "Single Jump range when unladen, see unladenMass",
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"unladenMass": {
|
||||
"description": "Mass of the Ship (hull + all components)",
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"definitions": {
|
||||
"standardRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H"] },
|
||||
"allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H", "I" ] }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user