Continued porting to React

This commit is contained in:
Colin McLeod
2016-01-11 18:04:38 -08:00
parent ab0019424f
commit 653cb30dd9
39 changed files with 1865 additions and 1420 deletions

View File

@@ -8,13 +8,15 @@ import Header from './components/Header';
import AboutPage from './pages/AboutPage'; import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage'; import NotFoundPage from './pages/NotFoundPage';
import OutfittingPage from './pages/OutfittingPage'; import OutfittingPage from './pages/OutfittingPage';
import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage'; import ShipyardPage from './pages/ShipyardPage';
export default class Coriolis extends React.Component { export default class Coriolis extends React.Component {
static childContextTypes = { static childContextTypes = {
language: React.PropTypes.object.isRequired, language: React.PropTypes.object.isRequired,
route: React.PropTypes.object sizeRatio: React.PropTypes.number.isRequired,
route: React.PropTypes.object.isRequired
}; };
constructor() { constructor() {
@@ -24,19 +26,21 @@ export default class Coriolis extends React.Component {
this._closeMenu = this._closeMenu.bind(this); this._closeMenu = this._closeMenu.bind(this);
this._showModal = this._showModal.bind(this); this._showModal = this._showModal.bind(this);
this._hideModal = this._hideModal.bind(this); this._hideModal = this._hideModal.bind(this);
this._onLanguageChange = this._onLanguageChange.bind(this) this._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this)
this._keyDown = this._keyDown.bind(this); this._keyDown = this._keyDown.bind(this);
this.state = { this.state = {
page: null, page: null,
language: getLanguage(Persist.getLangCode()), language: getLanguage(Persist.getLangCode()),
route: null route: null,
sizeRatio: Persist.getSizeRatio()
}; };
Router('', (r) => this._setPage(ShipyardPage, r)); Router('', (r) => this._setPage(ShipyardPage, r));
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r)); Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
// Router('/compare/:name', compare); Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
// Router('/comparison/:code', comparison); Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
Router('/about', (r) => this._setPage(AboutPage, r)); Router('/about', (r) => this._setPage(AboutPage, r));
Router('*', (r) => this._setPage(null, r)); Router('*', (r) => this._setPage(null, r));
} }
@@ -54,6 +58,10 @@ export default class Coriolis extends React.Component {
this.setState({ language: getLanguage(Persist.getLangCode()) }); this.setState({ language: getLanguage(Persist.getLangCode()) });
} }
_onSizeRatioChange(sizeRatio) {
this.setState({ sizeRatio });
}
_keyDown(e) { _keyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 27: 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) { _showModal(content) {
let modal = <div className='modal-bg' onClick={(e) => this._hideModal() }>{content}</div>; let modal = <div className='modal-bg' onClick={(e) => this._hideModal() }>{content}</div>;
this.setState({ modal }); this.setState({ modal });
} }
/**
* Hides any open modal
*/
_hideModal() { _hideModal() {
if (this.state.modal) { if (this.state.modal) {
this.setState({ modal: null }); this.setState({ modal: null });
} }
} }
/**
* Sets the open menu state
* @param {string|object} currentMenu The reference to the current menu
*/
_openMenu(currentMenu) { _openMenu(currentMenu) {
if (this.state.currentMenu != currentMenu) { if (this.state.currentMenu != currentMenu) {
this.setState({ currentMenu }); this.setState({ currentMenu });
} }
} }
/**
* Closes the open menu
*/
_closeMenu() { _closeMenu() {
if (this.state.currentMenu) { if (this.state.currentMenu) {
this.setState({ currentMenu: null }); 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() { getChildContext() {
return { return {
language: this.state.language, language: this.state.language,
route: this.state.route route: this.state.route,
sizeRatio: this.state.sizeRatio
}; };
} }
/**
* Adds necessary listeners and starts Routing
*/
componentWillMount() { componentWillMount() {
// Listen for appcache updated event, present refresh to update view // Listen for appcache updated event, present refresh to update view
if (window.applicationCache) { if (window.applicationCache) {
@@ -107,7 +138,7 @@ export default class Coriolis extends React.Component {
window.addEventListener('resize', InterfaceEvents.windowResized); window.addEventListener('resize', InterfaceEvents.windowResized);
document.addEventListener('keydown', this._keyDown); document.addEventListener('keydown', this._keyDown);
Persist.addListener('language', this._onLanguageChange); Persist.addListener('language', this._onLanguageChange);
Persist.addListener('language', this._onLanguageChange); Persist.addListener('sizeRatio', this._onSizeRatioChange);
InterfaceEvents.addListener('openMenu', this._openMenu); InterfaceEvents.addListener('openMenu', this._openMenu);
InterfaceEvents.addListener('closeMenu', this._closeMenu); InterfaceEvents.addListener('closeMenu', this._closeMenu);
InterfaceEvents.addListener('showModal', this._showModal); InterfaceEvents.addListener('showModal', this._showModal);
@@ -116,6 +147,10 @@ export default class Coriolis extends React.Component {
Router.start(); Router.start();
} }
/**
* Renders the main app
* @return {React.Component} The main app
*/
render() { render() {
return ( return (
<div onClick={this._closeMenu}> <div onClick={this._closeMenu}>

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

View File

@@ -11,28 +11,25 @@ export default class CostSection extends TranslatedComponent {
static PropTypes = { static PropTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
shipId: React.PropTypes.string.isRequired,
code: React.PropTypes.string.isRequired, code: React.PropTypes.string.isRequired,
buildName: React.PropTypes.string buildName: React.PropTypes.string
}; };
constructor(props) { constructor(props) {
super(props); super(props);
this._costsTab = this._costsTab.bind(this); 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 data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults
let retrofitName = props.buildName; let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName);
let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName);
let shipDiscount = Persist.getShipDiscount(); let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount(); 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); this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
retrofitShip.applyDiscounts(shipDiscount, moduleDiscount); retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
@@ -45,16 +42,35 @@ export default class CostSection extends TranslatedComponent {
total: props.ship.totalCost, total: props.ship.totalCost,
insurance: Insurance[Persist.getInsurance()], insurance: Insurance[Persist.getInsurance()],
tab: Persist.getCostTab(), tab: Persist.getCostTab(),
buildOptions: Persist.getBuildsNamesFor(props.shipId), buildOptions: Persist.getBuildsNamesFor(props.ship.id),
ammoPredicate: 'module', ammoPredicate: 'cr',
ammoDesc: true, ammoDesc: true,
costPredicate: 'cr', costPredicate: 'cr',
costDesc: true, costDesc: true,
retroPredicate: 'module', retroPredicate: 'cr',
retroDesc: true 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) { _showTab(tab) {
Persist.setCostTab(tab); Persist.setCostTab(tab);
this.setState({ tab }); this.setState({ tab });
@@ -72,72 +88,121 @@ export default class CostSection extends TranslatedComponent {
this.setState({ insurance: Insurance[insuranceName] }); this.setState({ insurance: Insurance[insuranceName] });
} }
_onBaseRetrofitChange(retrofitName) { /**
let existingBuild = Persist.getBuild(this.props.shipId, retrofitName); * Repopulate modules on retrofit ship from existing build
this.state.retrofitShip.buildFrom(existingBuild); // Repopulate modules 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 }); this.setState({ retrofitName });
} }
_onBuildSaved(shipId, name, code) { _onBuildSaved(shipId, name, code) {
if(this.state.retrofitName == name) { if(this.state.retrofitName == name) {
this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
} else { } 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) { _toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost); this.props.ship.setCostIncluded(item, !item.incCost);
this.setState({ total: this.props.ship.totalCost }); this.setState({ total: this.props.ship.totalCost });
} }
_sortCost(predicate) { _toggleRetrofitCost(item) {
let costList = this.props.ship.costList; 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; let { costPredicate, costDesc } = this.state;
if (predicate) {
if (costPredicate == predicate) { if (costPredicate == predicate) {
costDesc = !costDesc; costDesc = !costDesc;
} }
} else {
predicate = costPredicate;
}
if (predicate == 'm') {
let translate = this.context.language.translate;
costList.sort(nameComparator(translate));
} else {
costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0));
}
if (!costDesc) {
costList.reverse();
}
this.setState({ costPredicate: predicate, costDesc }); this.setState({ costPredicate: predicate, costDesc });
} }
_sortAmmo(predicate) { _sortCost(ship, predicate, desc) {
let { ammoPredicate, ammoDesc, ammoCosts } = this.state; 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) { if (ammoPredicate == predicate) {
ammoDesc = !ammoDesc; ammoDesc = !ammoDesc;
} }
switch (predicate) { this.setState({ ammoPredicate: predicate, ammoDesc });
case 'm': }
let translate = this.context.language.translate;
ammoCosts.sort(nameComparator(translate)); _sortAmmo(ammoCosts, predicate, desc) {
break;
default: if (predicate == 'm') {
ammoCosts.sort(nameComparator(this.context.language.translate));
} else {
ammoCosts.sort((a, b) => a[predicate] - b[predicate]); ammoCosts.sort((a, b) => a[predicate] - b[predicate]);
} }
if (!ammoDesc) { if (!desc) {
ammoCosts.reverse(); 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() { _costsTab() {
@@ -162,12 +227,12 @@ export default class CostSection extends TranslatedComponent {
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead> <thead>
<tr className='main'> <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')} {translate('component')}
{shipDiscount < 1 && <u className='optional-hide'>{`[${translate('ship')} -${formats.rPct(1 - shipDiscount)}]`}</u>} {shipDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct1(1 - shipDiscount)}]`}</u>}
{moduleDiscount < 1 && <u className='optional-hide'>{`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}</u>} {moduleDiscount < 1 && <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct1(1 - moduleDiscount)}]`}</u>}
</th> </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> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -185,11 +250,72 @@ export default class CostSection extends TranslatedComponent {
</div>; </div>;
} }
updateRetrofitCosts() { _retrofitTab() {
var costs = $scope.retrofitList = []; let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
var total = 0, i, l, item; 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'>&#9662;</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 = { item = {
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating, buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
buyName: ship.bulkheads.m.name, buyName: ship.bulkheads.m.name,
@@ -198,9 +324,9 @@ export default class CostSection extends TranslatedComponent {
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost, netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
retroItem: retrofitShip.bulkheads retroItem: retrofitShip.bulkheads
}; };
costs.push(item); retrofitCosts.push(item);
if (retrofitShip.bulkheads.incCost) { 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 retroSlotGroup = retrofitShip[g];
var slotGroup = ship[g]; var slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) { 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] }; 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.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating; item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating;
item.netCost = slotGroup[i].discountedCost; item.netCost = slotGroup[i].discountedCost;
} }
if (retroSlotGroup[i].id) { if (retroSlotGroup[i].m) {
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp; item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating; item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
item.netCost -= retroSlotGroup[i].discountedCost; item.netCost -= retroSlotGroup[i].discountedCost;
} }
costs.push(item); retrofitCosts.push(item);
if (retroSlotGroup[i].incCost) { if (retroSlotGroup[i].incCost) {
total += item.netCost; retrofitTotal += item.netCost;
} }
} }
} }
} }
$scope.retrofitTotal = total;
}
_retrofitTab() { this.setState({ retrofitCosts, retrofitTotal });
// return <div> this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
// <div className='scroll-x'>
// <table style='width:100%'>
// <thead>
// <tr className='main'>
// <th colspan='2' className='sortable le' ng-click='sortRetrofit('sellName | translate')' >{translate('sell')}</th>
// <th colspan='2' className='sortable le' ng-click='sortRetrofit('buyName | translate')' >{translate('buy')}</th>
// <th className='sortable le' ng-click='sortRetrofit('netCost')'>
// {{'net cost' | translate}} <u className='optional-hide' ng-if='discounts.components < 1'>[-{{fRPct(1 - discounts.components)}}]</u>
// </th>
// </tr>
// </thead>
// <tbody>
// <tr ng-if='!retrofitList || retrofitList.length == 0'>
// <td colspan='5' style='padding: 3em 0;' >{translate('PHRASE_NO_RETROCH')}</td>
// </tr>
// <tr className='highlight' ng-repeat='item in retrofitList | orderBy:retroPredicate:retroDesc' ng-click='toggleRetrofitCost(item.retroItem)' className={cn({disabled: !item.retroItem.incCost})}>
// <td style='width:1em;'>{{item.sellClassRating}}</td>
// <td className='le shorten cap'>{{item.sellName | translate}}</td>
// <td style='width:1em;'>{{item.buyClassRating}}</td>
// <td className='le shorten cap'>{{item.buyName | translate}}</td>
// <td className={cn('ri', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{{ fCrd(item.netCost)}} <u translate>CR</u></td>
// </tr>
// </tbody>
// </table>
// </div>
// <table className='total'>
// <tr className='ri'>
// <td className='lbl' >{translate('cost')}</td>
// <td colSpan={2} className={retrofitTotal > 0 ? 'warning' : 'secondary-disabled'}>{{fCrd(retrofitTotal)}} <u translate>CR</u></td>
// </tr>
// <tr className='ri'>
// <td className='lbl cap' >{translate('retrofit from')}</td>
// <td className='cen' style='border-right:none;width: 1em;'><u className='primary-disabled'>&#9662;</u></td>
// <td style='border-left:none;padding:0;'>
// <select style='width: 100%;padding: 0' ng-model='$parent.retrofitBuild' ng-change='setRetrofitBase()' ng-options='name as name for (name, build) in allBuilds[ship.id]'>
// <option value=''>{{'Stock' | translate}}</option>
// </select>
// </td>
// </tr>
// </table>
// </div>;
} }
_ammoTab() { _ammoTab() {
@@ -291,16 +374,16 @@ export default class CostSection extends TranslatedComponent {
<td className='ri'>{int(item.total)}{units.CR}</td> <td className='ri'>{int(item.total)}{units.CR}</td>
</tr>); </tr>);
} }
console.log(rows);
return <div> return <div>
<div className='scroll-x' > <div className='scroll-x' >
<table style={{ width: '100%', borderCollapse: 'collapse' }}> <table style={{ width: '100%' }}>
<thead> <thead>
<tr className='main'> <tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortAmmo.bind(this, 'm')} >{translate('module')}</th> <th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.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._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmo.bind(this, 'cost')} >{translate('unit cost')}</th> <th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
<th className='sortable le' onClick={this._sortAmmo.bind(this, 'total')}>{translate('total cost')}</th> <th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('total cost')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -318,14 +401,13 @@ export default class CostSection extends TranslatedComponent {
/** /**
* Recalculate all ammo costs * Recalculate all ammo costs
*/ */
_updateAmmoCosts() { _updateAmmoCosts(ship) {
let ship = this.props.ship; let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, scoop = false;
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) { for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
let slotGroup = ship[g]; let slotGroup = ship[g];
for (let i = 0, l = slotGroup.length; i < l; i++) { for (let i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].id) { if (slotGroup[i].m) {
//special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip //special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
q = 0; q = 0;
switch (slotGroup[i].m.grp) { switch (slotGroup[i].m.grp) {
@@ -338,6 +420,9 @@ export default class CostSection extends TranslatedComponent {
case 'am': case 'am':
q = slotGroup[i].m.ammo; q = slotGroup[i].m.ammo;
break; break;
case 'pv':
srvs += slotGroup[i].m.vehicles;
break;
case 'fx': case 'hb': case 'cc': case 'pc': case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity; limpets = ship.cargoCapacity;
break; break;
@@ -370,6 +455,17 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item); ammoCosts.push(item);
ammoTotal += item.total; 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 //calculate refuel costs if no scoop present
if (!scoop) { if (!scoop) {
item = { item = {
@@ -381,26 +477,66 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item); ammoCosts.push(item);
ammoTotal += item.total; ammoTotal += item.total;
} }
this.setState({ ammoTotal, ammoCosts }); this.setState({ ammoTotal, ammoCosts });
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
} }
componentWillMount(){ componentWillMount(){
this.listeners = [ this.listeners = [
Persist.addListener('discounts', this._onDiscountChanged.bind(this)), Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
Persist.addListener('insurance', this._onInsuranceChanged.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._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) { 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(){ componentWillUnmount(){
// remove window listener
this.listeners.forEach(l => l.remove()); this.listeners.forEach(l => l.remove());
} }

View File

@@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import { Languages } from '../i18n/Language';
import { Insurance, Discounts } from '../shipyard/Constants';
import Link from './Link'; import Link from './Link';
import ActiveLink from './ActiveLink'; import ActiveLink from './ActiveLink';
import cn from 'classnames'; import cn from 'classnames';
@@ -7,28 +9,58 @@ import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import InterfaceEvents from '../utils/InterfaceEvents'; import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import { toDetailedExport } from '../shipyard/Serializer';
import ModalDeleteAll from './ModalDeleteAll'; 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 { export default class Header extends TranslatedComponent {
constructor(props) { constructor(props, context) {
super(props); super(props);
this.shipOrder = Object.keys(Ships).sort(); 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) { _setInsurance(e) {
e.stopPropagation(); Persist.setInsurance(e.target.value);
Persist.setInsurance('beta'); // TODO: get insurance name
} }
_setModuleDiscount(e) { _setModuleDiscount(e) {
e.stopPropagation(); Persist.setModuleDiscount(e.target.value * 1);
Persist.setModuleDiscount(0); // TODO: get module discount
} }
_setShipDiscount(e) { _setShipDiscount(e) {
e.stopPropagation(); Persist.setShipDiscount(e.target.value * 1);
Persist.setShipDiscount(0); // TODO: get ship discount }
_setLanguage(e){
Persist.setLangCode(e.target.value);
} }
_showDeleteAll(e) { _showDeleteAll(e) {
@@ -37,29 +69,33 @@ export default class Header extends TranslatedComponent {
}; };
_showBackup(e) { _showBackup(e) {
let translate = this.context.language.translate;
e.preventDefault(); e.preventDefault();
/*$state.go('modal.export', { InterfaceEvents.showModal(<ModalExport
title: 'backup', title={translate('backup')}
data: Persist.getAll(), description={translate('PHRASE_BACKUP_DESC')}
description: 'PHRASE_BACKUP_DESC' data={Persist.getAll()}
});*/ />);
// TODO: implement modal
}; };
_showDetailedExport(e){ _showDetailedExport(e){
let translate = this.context.language.translate;
e.preventDefault(); e.preventDefault();
e.stopPropagation();
/*$state.go('modal.export', { InterfaceEvents.showModal(<ModalExport
title: 'detailed export', title={translate('detailed export')}
data: Serializer.toDetailedExport(Persist.getBuilds()), description={translate('PHRASE_EXPORT_DESC')}
description: 'PHRASE_EXPORT_DESC' data={toDetailedExport(Persist.getBuilds())}
});*/ />);
// TODO: implement modal }
_showImport(e) {
e.preventDefault();
InterfaceEvents.showModal(<ModalImport/>);
} }
_setTextSize(size) { _setTextSize(size) {
Persist.setSizeRatio(size); // TODO: implement properly Persist.setSizeRatio((size * SIZE_RANGE) + SIZE_MIN);
} }
_resetTextSize() { _resetTextSize() {
@@ -117,9 +153,8 @@ export default class Header extends TranslatedComponent {
let translate = this.context.language.translate; let translate = this.context.language.translate;
if (Persist.hasComparisons()) { if (Persist.hasComparisons()) {
let comparisons = []; comparisons = [];
let comps = Object.keys(Persist.getComparisons()).sort(); let comps = Object.keys(Persist.getComparisons()).sort();
console.log(comps);
for (let name of comps) { for (let name of comps) {
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className={'block name'}>{name}</ActiveLink>); comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className={'block name'}>{name}</ActiveLink>);
@@ -145,38 +180,54 @@ export default class Header extends TranslatedComponent {
<div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }> <div className={'menu-list no-wrap cap'} onClick={ (e) => e.stopPropagation() }>
<ul> <ul>
{translate('language')} {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><br/>
<ul> <ul>
{translate('insurance')} {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><br/>
<ul> <ul>
{translate('ship')} {translate('discount')} {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><br/>
<ul> <ul>
{translate('component')} {translate('discount')} {translate('module')} {translate('discount')}
<li><select className={'cap'} ng-model="discounts.components" ng-options="i for (i,d) in discounts.opts" ng-change="updateDiscount()"></select></li> <li>
<select className={'cap'} value={Persist.getModuleDiscount()} onChange={this._setModuleDiscount} >
{this.discountOptions}
</select>
</li>
</ul> </ul>
<hr /> <hr />
<ul> <ul>
{translate('builds')} & {translate('comparisons')} {translate('builds')} & {translate('comparisons')}
<li><a href="#" className={'block'} ng-click="backup($event)">{translate('backup')}</a></li> <li><a href="#" className={'block'} onClick={this._showBackup.bind(this)}>{translate('backup')}</a></li>
<li><a href="#" className={'block'} ng-click="detailedExport($event)">{translate('detailed export')}</a></li> <li><a href="#" className={'block'} onClick={this._showDetailedExport.bind(this)}>{translate('detailed export')}</a></li>
<li><a href="#" className={'block'} ui-sref="modal.import">{translate('import')}</a></li> <li><a href="#" className={'block'} onClick={this._showImport.bind(this)}>{translate('import')}</a></li>
<li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li> <li><a href="#" onClick={this._showDeleteAll.bind(this)}>{translate('delete all')}</a></li>
</ul> </ul>
<hr /> <hr />
<table style={{width: 300, backgroundColor: 'transparent'}}> <table style={{width: 300, backgroundColor: 'transparent'}}>
<tbody> <tbody>
<tr> <tr>
<td style={{width: 20}}><u>A</u></td> <td style={{ width: '1em', verticalAlign: 'top' }}><u>A</u></td>
<td slider min="0.65" def="sizeRatio" max="1.2" on-change="textSizeChange(val)" ignore-resize="true"></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> <td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
</tr> </tr>
<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> </tr>
</tbody> </tbody>
</table> </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() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let openedMenu = this.props.currentMenu; let openedMenu = this.props.currentMenu;

View File

@@ -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.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{u.T}</div> : null }
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null } { m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{u.T}</div> : null }
{ m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null } { m.bins ? <div className={'l'}>{m.bins + ' '}<u>{translate('bins')}</u></div> : null }
{ m.bays ? <div className={'l'}>{translate('bays') + ': ' + m.bays}</div> : null }
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null } { m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
{ m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null } { m.ammo ? <div className={'l'}>{translate('ammo')}: {formats.gen(m.ammo)}</div> : null }
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null } { m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }

View File

@@ -83,7 +83,6 @@ export default class InternalSlotSection extends SlotSection {
enabled={s.enabled} enabled={s.enabled}
m={s.m} m={s.m}
fuel={fuelCapacity} fuel={fuelCapacity}
shipMass={ladenMass}
/>); />);
} }

View File

@@ -1,7 +1,10 @@
import React from 'react'; import React from 'react';
import { findDOMNode } from 'react-dom';
import d3 from 'd3';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
const RENDER_POINTS = 20; // Only render 20 points on the graph const RENDER_POINTS = 20; // Only render 20 points on the graph
const MARGIN = { top: 15, right: 15, bottom: 35, left: 60 }
export default class LineChart extends TranslatedComponent { export default class LineChart extends TranslatedComponent {
@@ -12,40 +15,201 @@ export default class LineChart extends TranslatedComponent {
} }
static PropTypes = { static PropTypes = {
xMax: React.PropTypes.number.isRequired, width: React.PropTypes.number.isRequired,
yMax: React.PropTypes.number.isRequired,
func: React.PropTypes.func.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, series: React.PropTypes.array,
colors: 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); 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(){ componentWillMount(){
// Listen to window resize this._updateDimensions(this.props, this.context.sizeRatio);
} this._updateSeriesData(this.props);
componentWillUnmount(){
// remove window listener
// remove mouse move listener / touch listner?
} }
componentWillReceiveProps(nextProps, nextContext) { 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() { 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>;
} }
} }

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

View File

@@ -7,7 +7,7 @@ export default class ModalExport extends TranslatedComponent {
static propTypes = { static propTypes = {
title: React.PropTypes.string, title: React.PropTypes.string,
promise: React.PropTypes.func, 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) { constructor(props) {

View File

@@ -3,7 +3,7 @@ import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents'; import InterfaceEvents from '../utils/InterfaceEvents';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import Ships from '../shipyard/Ships'; import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ModuleUtils from '../shipyard/ModuleUtils';
import { Download } from './SvgIcons'; import { Download } from './SvgIcons';
@@ -36,7 +36,8 @@ function validateBuild(shipId, code, name) {
throw shipData.properties.name + ' build "' + name + '" is not valid!'; throw shipData.properties.name + ' build "' + name + '" is not valid!';
} }
try { 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) { } catch (e) {
throw shipData.properties.name + ' build "' + name + '" is not valid!'; throw shipData.properties.name + ' build "' + name + '" is not valid!';
} }
@@ -58,17 +59,11 @@ function detailedJsonToBuild(detailedBuild) {
throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data'; 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 { 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) { constructor(props) {
super(props); super(props);
@@ -84,7 +79,11 @@ export default class ModalImport extends TranslatedComponent {
}; };
this._process = this._process.bind(this); this._process = this._process.bind(this);
this._import = this._import.bind(this);
this._importBackup = this._importBackup.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) { _importBackup(importData) {
@@ -121,7 +120,7 @@ export default class ModalImport extends TranslatedComponent {
_importDetailedArray(importArr) { _importDetailedArray(importArr) {
let builds = {}; let builds = {};
for (let i = 0, l = importArr.length; i < l; i++) { for (let i = 0, l = importArr.length; i < l; i++) {
let build = this._detailedJsonToBuild(importArr[i]); let build = detailedJsonToBuild(importArr[i]);
if (!builds[build.shipId]) { if (!builds[build.shipId]) {
builds[build.shipId] = {}; builds[build.shipId] = {};
} }
@@ -226,16 +225,17 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ builds }); this.setState({ builds });
} }
_validateImport() { _validateImport(e) {
let importData = null; let importData = null;
let importString = $scope.importString.trim(); let importString = e.target.value;
this.setState({ this.setState({
builds: null, builds: null,
comparisons: null, comparisons: null,
discounts: null, discounts: null,
errorMsg: null, errorMsg: null,
importValid: false, importValid: false,
insurance: null insurance: null,
importString,
}); });
if (!importString) { if (!importString) {
@@ -246,7 +246,7 @@ export default class ModalImport extends TranslatedComponent {
if (textBuildRegex.test(importString)) { // E:D Shipyard build text if (textBuildRegex.test(importString)) { // E:D Shipyard build text
importTextBuild(importString); importTextBuild(importString);
} else { // JSON Build data } else { // JSON Build data
importData = angular.fromJson($scope.importString); importData = JSON.parse(importString);
if (!importData || typeof importData != 'object') { if (!importData || typeof importData != 'object') {
throw 'Must be an object or array!'; throw 'Must be an object or array!';
@@ -272,7 +272,7 @@ export default class ModalImport extends TranslatedComponent {
let builds = null, comparisons = null; let builds = null, comparisons = null;
if (this.state.builds) { if (this.state.builds) {
builds = $scope.builds; builds = this.state.builds;
for (let shipId in builds) { for (let shipId in builds) {
for (let buildName in builds[shipId]) { for (let buildName in builds[shipId]) {
let code = builds[shipId][buildName]; let code = builds[shipId][buildName];
@@ -286,7 +286,7 @@ export default class ModalImport extends TranslatedComponent {
} }
if (this.state.comparisons) { if (this.state.comparisons) {
let comparisons = $scope.comparisons; let comparisons = this.state.comparisons;
for (let name in comparisons) { for (let name in comparisons) {
comparisons[name].useName = name; comparisons[name].useName = name;
} }
@@ -341,19 +341,20 @@ export default class ModalImport extends TranslatedComponent {
render() { render() {
let translate = this.context.language.translate; let translate = this.context.language.translate;
let state = this.state;
let importStage; let importStage;
if (this.state.processed) { if (!state.processed) {
importStage = ( importStage = (
<div> <div>
<textarea className='cb json' onCange={this.validateImport.bind(this)} placeholder={translate('PHRASE_IMPORT') | translate} /> <textarea className='cb json' onChange={this._validateImport} defaultValue={this.state.importString} placeholder={translate('PHRASE_IMPORT')} />
<button className='l cap' onClick={this.process.bind(this)} disabled={!this.state.importValid} >{translate('proceed')}</button> <button className='l cap' onClick={this._process} disabled={!state.importValid} >{translate('proceed')}</button>
<div className='l warning' style={{ marginLeft:'3em' }}>{this.state.errorMsg}</div> <div className='l warning' style={{ marginLeft:'3em' }}>{state.errorMsg}</div>
</div> </div>
); );
} else { } else {
let comparisonTable, edit, buildRows = []; let comparisonTable, edit, buildRows = [];
if (this.state.comparisons) { if (state.comparisons) {
let comparisonRows = []; let comparisonRows = [];
for (let name in comparisons) { for (let name in comparisons) {
@@ -387,7 +388,7 @@ export default class ModalImport extends TranslatedComponent {
} }
if(this.state.canEdit) { 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; let builds = this.state.builds;
@@ -410,11 +411,11 @@ export default class ModalImport extends TranslatedComponent {
importStage = ( importStage = (
<div> <div>
<table className='l' style='overflow:hidden;margin: 1em 0; width: 100%;'> <table className='l' style={{ overflow:'hidden', margin: '1em 0', width: '100%'}}>
<thead> <thead>
<tr> <tr>
<th style='text-align:left' >{translate('ship')}</th> <th style={{ textAlign: 'left' }} >{translate('ship')}</th>
<th style='text-align:left' >{translate('build name')}</th> <th style={{ textAlign: 'left' }} >{translate('build name')}</th>
<th >{translate('action')}</th> <th >{translate('action')}</th>
</tr> </tr>
</thead> </thead>
@@ -422,9 +423,7 @@ export default class ModalImport extends TranslatedComponent {
{buildRows} {buildRows}
</tbody> </tbody>
</table> </table>
{comparisonTable} {comparisonTable}
<button className='cl l' onClick={this._import}><Download/> {translate('import')}</button> <button className='cl l' onClick={this._import}><Download/> {translate('import')}</button>
{edit} {edit}
</div> </div>

View File

@@ -1,10 +1,7 @@
import React from 'react'; import React from 'react';
import { findDOMNode } from 'react-dom';
import d3 from 'd3'; import d3 from 'd3';
import cn from 'classnames'; import cn from 'classnames';
import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import { wrapCtxMenu } from '../utils/InterfaceEvents'; import { wrapCtxMenu } from '../utils/InterfaceEvents';
/** /**
@@ -27,7 +24,8 @@ export default class PowerBands extends TranslatedComponent {
static propTypes = { static propTypes = {
bands: React.PropTypes.array.isRequired, bands: React.PropTypes.array.isRequired,
available: React.PropTypes.number.isRequired, available: React.PropTypes.number.isRequired,
code: React.PropTypes.string width: React.PropTypes.number.isRequired,
code: React.PropTypes.string,
}; };
constructor(props, context) { 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.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.pctAxis = d3.svg.axis().scale(this.pctScale).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.rPct);
this._updateWidth = this._updateWidth.bind(this); this._updateDimensions = this._updateDimensions.bind(this);
this._updateAxes = this._updateAxes.bind(this); this._updateScales = this._updateScales.bind(this);
this._selectNone = this._selectNone.bind(this); this._selectNone = this._selectNone.bind(this);
let maxBand = props.bands[props.bands.length - 1]; let maxBand = props.bands[props.bands.length - 1];
this.state = { this.state = {
outerWidth: 0,
innerWidth: 0,
sizes: this._initSizes(Persist.getSizeRatio()),
maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum), maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum),
ret: {}, ret: {},
dep: {} dep: {}
}; };
if (props.width) {
this._updateDimensions(props, context.sizeRatio);
}
} }
_initSizes(size) { _updateDimensions(props, size) {
let barHeight = Math.round(20 * size); let barHeight = Math.round(20 * size);
let innerHeight = (barHeight * 2) + 2; let innerHeight = (barHeight * 2) + 2;
let mTop = Math.round(25 * size); let mTop = Math.round(25 * size);
let mBottom = 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, barHeight,
innerHeight, innerHeight,
mTop, mTop,
mBottom, mBottom,
mLeft: Math.round(45 * size), mLeft,
mRight: Math.round(130 * size), mRight,
innerWidth,
height: innerHeight + mTop + mBottom, height: innerHeight + mTop + mBottom,
retY: (barHeight / 2), retY: (barHeight / 2),
depY: (barHeight * 1.5) - 1 depY: (barHeight * 1.5) - 1
}; });
} }
_selectNone() { _selectNone() {
@@ -102,34 +107,18 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ dep: Object.assign({}, dep) }); 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.wattScale.range([0, innerWidth]).domain([0,maxPwr]).clamp(true);
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true); this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
} }
_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) { componentWillReceiveProps(nextProps, nextContext) {
let { innerWidth, maxPwr } = this.state; let { innerWidth, maxPwr } = this.state;
let maxBand = nextProps.bands[nextProps.bands.length - 1]; let maxBand = nextProps.bands[nextProps.bands.length - 1];
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum); let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed if (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 }); this.setState({ maxPwr: nextMaxPwr });
} }
@@ -137,19 +126,22 @@ export default class PowerBands extends TranslatedComponent {
this.wattAxis.tickFormat(nextContext.language.formats.r2); this.wattAxis.tickFormat(nextContext.language.formats.r2);
this.pctAxis.tickFormat(nextContext.language.formats.rPct); this.pctAxis.tickFormat(nextContext.language.formats.rPct);
} }
}
componentWillUnmount(){ if (nextProps.width != this.props.width || this.context !== nextContext) {
this.resizeListener.remove(); this._updateDimensions(nextProps, nextContext.sizeRatio);
this.sizeListener.remove(); }
} }
render() { render() {
if (!this.props.width) {
return null;
}
let { wattScale, pctScale, context, props, state } = this; let { wattScale, pctScale, context, props, state } = this;
let { translate, formats } = context.language; let { translate, formats } = context.language;
let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis let { f2, pct1, rPct, r2 } = formats; // wattFmt, pctFmt, pctAxis, wattAxis
let { available, bands } = props; let { available, bands, width } = props;
let { sizes, outerWidth, innerWidth, maxPwr, ret, dep } = state; let { innerWidth, maxPwr, ret, dep } = state;
let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available }); let pwrWarningClass = cn('threshold', {exceeded: bands[0].retractedSum * 2 >= available });
let deployed = []; let deployed = [];
let retracted = []; let retracted = [];
@@ -158,19 +150,18 @@ export default class PowerBands extends TranslatedComponent {
let retSum = 0; let retSum = 0;
let depSum = 0; let depSum = 0;
if (outerWidth > 0) {
for (var i = 0; i < bands.length; i++) { for (var i = 0; i < bands.length; i++) {
let b = bands[i]; let b = bands[i];
retSum += (!retSelected || ret[i]) ? b.retracted : 0; retSum += (!retSelected || ret[i]) ? b.retracted : 0;
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0; depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
if (outerWidth && b.retracted > 0) { if (b.retracted > 0) {
let retLbl = bandText(b.retracted, i, wattScale); let retLbl = bandText(b.retracted, i, wattScale);
retracted.push(<rect retracted.push(<rect
key={'rB' + i} key={'rB' + i}
width={Math.ceil(Math.max(wattScale(b.retracted), 0))} width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
height={sizes.barHeight} height={state.barHeight}
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))} x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
y={1} y={1}
onClick={this._selectRet.bind(this, i)} onClick={this._selectRet.bind(this, i)}
@@ -182,24 +173,24 @@ export default class PowerBands extends TranslatedComponent {
key={'rT' + i} key={'rT' + i}
dy='0.5em' dy='0.5em'
textAnchor='middle' textAnchor='middle'
height={sizes.barHeight} height={state.barHeight}
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)} x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
y={sizes.retY} y={state.retY}
onClick={this._selectRet.bind(this, i)} onClick={this._selectRet.bind(this, i)}
className='primary-bg'>{retLbl}</text> className='primary-bg'>{retLbl}</text>
); );
} }
} }
if (outerWidth && (b.retracted > 0 || b.deployed > 0)) { if (b.retracted > 0 || b.deployed > 0) {
let depLbl = bandText(b.deployed + b.retracted, i, wattScale); let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
deployed.push(<rect deployed.push(<rect
key={'dB' + i} key={'dB' + i}
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))} width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
height={sizes.barHeight} height={state.barHeight}
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))} x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
y={sizes.barHeight + 1} y={state.barHeight + 1}
onClick={this._selectDep.bind(this, i)} onClick={this._selectDep.bind(this, i)}
className={getClass(dep[i], b.deployedSum, available)} className={getClass(dep[i], b.deployedSum, available)}
/>); />);
@@ -209,20 +200,19 @@ export default class PowerBands extends TranslatedComponent {
key={'dT' + i} key={'dT' + i}
dy='0.5em' dy='0.5em'
textAnchor='middle' textAnchor='middle'
height={sizes.barHeight} height={state.barHeight}
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)} x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
y={sizes.depY} y={state.depY}
onClick={this._selectDep.bind(this, i)} onClick={this._selectDep.bind(this, i)}
className='primary-bg'>{depLbl}</text> className='primary-bg'>{depLbl}</text>
); );
} }
} }
} }
}
return ( return (
<svg style={{ marginTop: '1em', width: '100%', height: sizes.height }} onContextMenu={wrapCtxMenu(this._selectNone)}> <svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
<g transform={`translate(${sizes.mLeft},${sizes.mTop})`}> <g transform={`translate(${state.mLeft},${state.mTop})`}>
<g className='power-band'>{retracted}</g> <g className='power-band'>{retracted}</g>
<g className='power-band'>{deployed}</g> <g className='power-band'>{deployed}</g>
<g ref={ (elem) => d3.select(elem).call(this.wattAxis) } className='watt axis'></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.call(this.pctAxis);
axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass); axis.select('g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass);
}} }}
className='pct axis' transform={`translate(0,${sizes.innerHeight})`}></g> className='pct axis' transform={`translate(0,${state.innerHeight})`}></g>
<line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={sizes.innerHeight} className={pwrWarningClass} /> <line x1={pctScale(0.5)} x2={pctScale(0.5)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
<text dy='0.5em' x='-3' y={sizes.retY} className='primary upp' textAnchor='end'>{translate('ret')}</text> <text dy='0.5em' x='-3' y={state.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='-3' y={state.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={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={sizes.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text> <text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum)) + ' (' + pct1(Math.max(0, depSum / available)) + ')'}</text>
</g> </g>
</svg> </svg>
); );

View File

@@ -1,6 +1,8 @@
import React from 'react'; import React from 'react';
import { findDOMNode } from 'react-dom';
import cn from 'classnames'; import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import InterfaceEvents from '../utils/InterfaceEvents';
import PowerBands from './PowerBands'; import PowerBands from './PowerBands';
import { slotName, nameComparator } from '../utils/SlotFunctions'; import { slotName, nameComparator } from '../utils/SlotFunctions';
import { Power, NoPower } from './SvgIcons'; import { Power, NoPower } from './SvgIcons';
@@ -22,61 +24,43 @@ export default class PowerManagement extends TranslatedComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this._renderPowerRows = this._renderPowerRows.bind(this); this._renderPowerRows = this._renderPowerRows.bind(this);
this._sortName = this._sortName.bind(this); this._updateWidth = this._updateWidth.bind(this);
this._sortType = this._sortType.bind(this); this._sort = this._sort.bind(this);
this._sortPriority = this._sortPriority.bind(this);
this._sortPower = this._sortPower.bind(this);
this._sortRetracted = this._sortRetracted.bind(this);
this._sortDeployed = this._sortDeployed.bind(this);
this.state = {}; // State is initialized through componentWillMount this.state = {
predicate: 'n',
desc: true,
width: 0
};
} }
_sortOrder(predicate) { _sortOrder(predicate) {
let desc = this.state.desc; let desc = this.state.desc;
if (predicate == this.state.predicate) { if (predicate == this.state.predicate) {
desc = !desc; desc = !desc;
} else { } else {
desc = true; desc = true;
} }
if (!desc) {
this.props.ship.powerList.reverse();
}
this.setState({ predicate, desc }); this.setState({ predicate, desc });
} }
_sortName() { _sort(ship, predicate, desc) {
let translate = this.context.language.translate; let powerList = ship.powerList;
this.props.ship.powerList.sort(nameComparator(translate));
this._sortOrder('n'); 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;
} }
_sortType() { if (!desc) {
this.props.ship.powerList.sort((a, b) => a.type.localeCompare(b.type)); powerList.reverse();
this._sortOrder('t');
} }
_sortPriority() {
this.props.ship.powerList.sort((a, b) => a.priority - b.priority);
this._sortOrder('pri');
}
_sortPower() {
this.props.ship.powerList.sort((a, b) => (a.m ? a.m.power : 0) - (b.m ? b.m.power : 0));
this._sortOrder('pwr');
}
_sortRetracted() {
let ship = this.props.ship;
this.props.ship.powerList.sort((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b));
this._sortOrder('ret');
}
_sortDeployed() {
let ship = this.props.ship;
this.props.ship.powerList.sort((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true));
this._sortOrder('dep');
} }
_priority(slot, inc) { _priority(slot, inc) {
@@ -128,14 +112,30 @@ export default class PowerManagement extends TranslatedComponent {
return powerRows; return powerRows;
} }
_updateWidth() {
this.setState({ width: findDOMNode(this).offsetWidth });
}
componentWillMount(){ componentWillMount(){
this._sortName(); this._sort(this.props.ship, this.state.predicate, this.state.desc);
// Listen to window resize 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(){ componentWillUnmount(){
// remove window listener this.resizeListener.remove();
// remove mouse move listener / touch listner?
} }
render() { render() {
@@ -143,18 +143,19 @@ export default class PowerManagement extends TranslatedComponent {
let { translate, formats } = this.context.language; let { translate, formats } = this.context.language;
let pwr = formats.f2; let pwr = formats.f2;
let pp = ship.standard[0].m; let pp = ship.standard[0].m;
let sortOrder = this._sortOrder;
return ( return (
<div className='group half' id='componentPriority'> <div className='group half' id='componentPriority'>
<table style={{ width: '100%' }}> <table style={{ width: '100%' }}>
<thead> <thead>
<tr className='main'> <tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortName} >{translate('module')}</th> <th colSpan='2' className='sortable le' onClick={sortOrder.bind(this, 'n')} >{translate('module')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortType} >{translate('type')}</th> <th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 't')} >{translate('type')}</th>
<th style={{ width: '4em' }} className='sortable' onClick={this._sortPriority} >{translate('pri')}</th> <th style={{ width: '4em' }} className='sortable' onClick={sortOrder.bind(this, 'pri')} >{translate('pri')}</th>
<th colSpan='2' className='sortable' onClick={this._sortPower} >{translate('PWR')}</th> <th colSpan='2' className='sortable' onClick={sortOrder.bind(this, 'pwr')} >{translate('PWR')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortRetracted} >{translate('ret')}</th> <th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'r')} >{translate('ret')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={this._sortDeployed} >{translate('dep')}</th> <th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'd')} >{translate('dep')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -172,7 +173,7 @@ export default class PowerManagement extends TranslatedComponent {
{this._renderPowerRows(ship, translate, pwr, formats.pct1)} {this._renderPowerRows(ship, translate, pwr, formats.pct1)}
</tbody> </tbody>
</table> </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> </div>
); );
} }

View File

@@ -3,7 +3,6 @@ import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames'; import cn from 'classnames';
import { SizeMap } from '../shipyard/Constants'; import { SizeMap } from '../shipyard/Constants';
import { Warning } from './SvgIcons'; import { Warning } from './SvgIcons';
import shallowEqual from '../utils/shallowEqual';
export default class ShipSummaryTable extends TranslatedComponent { export default class ShipSummaryTable extends TranslatedComponent {
@@ -63,8 +62,8 @@ export default class ShipSummaryTable extends TranslatedComponent {
<tr> <tr>
<td className='cap'>{translate(SizeMap[ship.class])}</td> <td className='cap'>{translate(SizeMap[ship.class])}</td>
<td>{ship.agility}/10</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.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</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.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{round(ship.totalDps)}</td> <td>{round(ship.totalDps)}</td>
<td>{int(ship.armour)} {armourDetails}</td> <td>{int(ship.armour)} {armourDetails}</td>
<td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td> <td>{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? <u>({formats.rPct(ship.shieldMultiplier)})</u> : null }</td>

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

View File

@@ -42,7 +42,7 @@ export default class StandardSlot extends TranslatedComponent {
<div className={'cb'}> <div className={'cb'}>
{ m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null } { m.optmass ? <div className={'l'}>{translate('optimal mass') + ': '}{m.optmass}{units.T}</div> : null }
{ m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null } { m.maxmass ? <div className={'l'}>{translate('max mass') + ': '}{m.maxmass}{units.T}</div> : null }
{ m.range ? <div className={'l'}>{translate('range')} {m.range}{units.km}</div> : null } { m.range ? <div className={'l'}>{translate('range')}: {m.range}{units.km}</div> : null }
{ m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null } { m.time ? <div className={'l'}>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</div> : null } { m.eff ? <div className={'l'}>{translate('efficiency')}: {m.eff}</div> : null }
{ m.pGen ? <div className={'l'}>{translate('power')}: {m.pGen}{units.MW}</div> : null } { m.pGen ? <div className={'l'}>{translate('power')}: {m.pGen}{units.MW}</div> : null }

View File

@@ -1,21 +1,44 @@
import React from 'react'; import React from 'react';
import shallowEqual from '../utils/shallowEqual'; import shallowEqual from '../utils/shallowEqual';
/**
* @class Abstract TranslatedComponent
*/
export default class TranslatedComponent extends React.Component { export default class TranslatedComponent extends React.Component {
static contextTypes = { static contextTypes = {
language: React.PropTypes.object.isRequired language: React.PropTypes.object.isRequired,
sizeRatio: React.PropTypes.number.isRequired
} }
/**
* Created an instance of a Translated Component. This is an abstract class.
* @param {object} props Properties
*/
constructor(props) { constructor(props) {
super(props); super(props);
this.didContextChange = this.didContextChange.bind(this); 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){ 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) { shouldComponentUpdate(nextProps, nextState, nextContext) {
return !shallowEqual(this.props, nextProps) return !shallowEqual(this.props, nextProps)
|| !shallowEqual(this.state, nextState) || !shallowEqual(this.state, nextState)

View File

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

View File

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

View File

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

View File

@@ -9,6 +9,11 @@ import d3 from 'd3';
let fallbackTerms = EN.terms; 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) { export function getLanguage(langCode) {
let lang, translate; let lang, translate;
@@ -53,15 +58,20 @@ export function getLanguage(langCode) {
Ls: <u>{' ' + translate('Ls')}</u>, // Light Seconds Ls: <u>{' ' + translate('Ls')}</u>, // Light Seconds
LY: <u>{' ' + translate('LY')}</u>, // Light Years LY: <u>{' ' + translate('LY')}</u>, // Light Years
MJ: <u>{' ' + translate('MJ')}</u>, // Mega Joules 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) MW: <u>{' ' + translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
ps: <u>{translate('/s')}</u>, // per second ps: <u>{translate('/s')}</u>, // per second
pm: <u>{translate('/min')}</u>, // per minute
T: <u>{' ' + translate('T')}</u>, // Metric Tons T: <u>{' ' + translate('T')}</u>, // Metric Tons
} }
} }
} }
/**
* The list of available languages
* @type {Object}
*/
export const Languages = { export const Languages = {
en: 'English', en: 'English',
de: 'Deutsch', de: 'Deutsch',

View File

@@ -1,215 +1,344 @@
import React from 'react'; import React from 'react';
import Page from './Page'; import Page from './Page';
import Ships from '../shipyard/Ships'; import Router from '../Router';
import cn from 'classnames'; import cn from 'classnames';
import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import * as ModuleUtils from '../shipyard/ModuleUtils'; import Serializer from '../shipyard/Serializer';
import { SizeMap } from '../shipyard/Constants'; import InterfaceEvents from '../utils/InterfaceEvents';
import Link from '../components/Link'; 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 { export default class ComparisonPage extends Page {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = { this._sortShips = this._sortShips.bind(this);
title: 'Coriolis - Shipyard', this._buildsSelected = this._buildsSelected.bind(this);
shipPredicate: 'name', this.state = this._initState(props, context);
shipDesc: false }
};
this.context = context;
this.shipSummaries = [];
for (let s in Ships) { _initState(props, context) {
this.shipSummaries.push(this._shipSummary(s, Ships[s])); 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 * Sort ships
* @param {object} key Sort predicate * @param {object} key Sort predicate
*/ */
_sortShips(shipPredicate, shipPredicateIndex) { _sortShips(predicate) {
let shipDesc = this.state.shipDesc; let { builds, desc } = this.state;
if (this.state.predicate == predicate) {
if (typeof shipPredicateIndex == 'object') { desc = !desc;
shipPredicateIndex = undefined;
} }
if (this.state.shipPredicate == shipPredicate && this.state.shipPredicateIndex == shipPredicateIndex) { builds.sort(sortBy(predicate));
shipDesc = !shipDesc;
if (desc) {
builds.reverse();
} }
this.setState({ shipPredicate, shipDesc, shipPredicateIndex }); this.setState({ predicate, desc });
}; };
_shipRowElement(s, translate, u, fInt, fRound) { _selectBuilds() {
return <tr key={s.id} className={'highlight'}> InterfaceEvents.showModal(React.cloneElement(
<td className={'le'}><Link href={'/outfitting/' + s.id}>{s.name}</Link></td> <ModalCompare onSelect={this._buildsSelected}/>,
<td className={'le'}>{s.manufacturer}</td> { builds: this.state.builds }
<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>;
} }
_renderSummaries(language) { _buildsSelected(newBuilds) {
let fInt = language.formats.int; InterfaceEvents.hideModal();
let fRound = language.formats.round; let builds = [];
let translate = language.translate;
let u = language.units; for (let b of newBuilds) {
// Regenerate ship rows on prop change builds.push(this._createBuild(b.id, b.buildName));
for (let s of this.shipSummaries) { }
s.rowElement = this._shipRowElement(s, translate, u, fInt, fRound);
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) { _onNameChange(e) {
if (this.context.language !== nextContext.language) { this.setState({ newName: e.target.value, saved: false });
this._renderSummaries(language); }
_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() { 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 translate = this.context.language.translate;
let compareHeader;
let {newName, name, saved, builds, facets, predicate, desc } = this.state;
for (let s of shipSummaries) { if (this.state.compareMode) {
shipRows.push(s.rowElement); 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 ( return (
<div className={'page'}> <div className={'page'} style={{ fontSize: this.context.sizeRatio + 'em'}}>
{/*<table id='comparison'> <table id='comparison'>
<tr ng-show='compareMode'> <tbody>
<td class='head' translate='comparison'></td> {compareHeader}
<tr key='facets'>
<td className='head'>{translate('compare')}</td>
<td> <td>
<input ng-model='name' ng-change='nameChange()' placeholder={translate('enter name')} maxlength={50} /> <ul id='facet-container' onDragOver={this._facetDragOver}>
<button ng-click='save()'disabled{!name || name == 'all' || saved}> {facets.map((f, i) =>
<svg class='icon lg '><use xlink:href='#floppy-disk'></use></svg><span class='button-lbl'> {{'save' | translate}}</span> <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)}>
</button> {'↔ ' + translate(f.title)}
<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>&#x2194; <span ng-bind='f.title | translate'></span></div>
</li> </li>
)}
</ul> </ul>
</td> </td>
</tr> </tr>
</tbody>
</table> </table>
<div class='scroll-x'> <ComparisonTable builds={builds} facets={facets} onSort={this._sortShips} predicate={predicate} desc={desc} />
<table id='comp-tbl' comparison-table ng-click='handleClick($event)'></table>
</div>
<div ng-repeat='f in facets | filter:{active:true}' ng-if='builds.length > 0' class='chart' bar-chart facet='f' data='builds'> {/*<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> <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> </div>
); );
} }

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { findDOMNode } from 'react-dom';
import Page from './Page'; import Page from './Page';
import cn from 'classnames'; import cn from 'classnames';
import Router from '../Router'; import Router from '../Router';
@@ -7,7 +8,7 @@ import InterfaceEvents from '../utils/InterfaceEvents';
import { Ships } from 'coriolis-data'; import { Ships } from 'coriolis-data';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer'; import { toDetailedBuild } from '../shipyard/Serializer';
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 ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection'; import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection'; import HardpointsSlotSection from '../components/HardpointsSlotSection';
@@ -17,6 +18,7 @@ import LineChart from '../components/LineChart';
import PowerManagement from '../components/PowerManagement'; import PowerManagement from '../components/PowerManagement';
import CostSection from '../components/CostSection'; import CostSection from '../components/CostSection';
import ModalExport from '../components/ModalExport'; import ModalExport from '../components/ModalExport';
import Slider from '../components/Slider';
const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips']; const SPEED_SERIES = ['boost', '4 Pips', '2 Pips', '0 Pips'];
const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400']; const SPEED_COLORS = ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'];
@@ -50,6 +52,8 @@ export default class OutfittingPage extends Page {
ship.buildWith(data.defaults); // Populate with default components ship.buildWith(data.defaults); // Populate with default components
} }
let fuelCapacity = ship.fuelCapacity;
return { return {
title: 'Outfitting - ' + data.properties.name, title: 'Outfitting - ' + data.properties.name,
costTab: Persist.getCostTab() || 'costs', costTab: Persist.getCostTab() || 'costs',
@@ -57,7 +61,12 @@ export default class OutfittingPage extends Page {
shipId, shipId,
ship, ship,
code, 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() { _shipUpdated() {
let { shipId, buildName } = this.state; let { shipId, buildName, ship, fuelCapacity } = this.state;
let newCode = this.state.ship.toString(); let code = ship.toString();
this._updateRoute(shipId, newCode, buildName);
this.setState({ code: newCode }); if (fuelCapacity != ship.fuelCapacity) {
this._fuelChange(this.state.fuelLevel);
}
this._updateRoute(shipId, code, buildName);
this.setState({ code });
} }
_updateRoute(shipId, code, buildName) { _updateRoute(shipId, code, buildName) {
@@ -123,16 +137,47 @@ export default class OutfittingPage extends Page {
Router.replace(`/outfit/${shipId}/${code}${qStr}`); 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) { componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextContext)); this.setState(this._initState(nextContext));
} }
} }
componentWillMount(){
this.resizeListener = InterfaceEvents.addListener('windowResized', this._updateDimensions);
}
componentDidMount(){
this._updateDimensions();
}
componentWillUnmount(){
this.resizeListener.remove();
}
render() { render() {
let { translate, units } = this.context.language; let { translate, units, formats } = this.context.language;
let state = this.state; let state = this.state;
let { ship, code, savedCode, buildName } = state; let { ship, code, savedCode, buildName, chartWidth } = state;
let menu = this.props.currentMenu; let menu = this.props.currentMenu;
let shipUpdated = this._shipUpdated; let shipUpdated = this._shipUpdated;
let hStr = ship.getHardpointsString(); let hStr = ship.getHardpointsString();
@@ -140,7 +185,7 @@ export default class OutfittingPage extends Page {
let iStr = ship.getInternalString(); let iStr = ship.getInternalString();
return ( return (
<div id='outfit' className={'page'}> <div id='outfit' className={'page'} style={{ fontSize: (this.context.sizeRatio * 0.9) + 'em'}}>
<div id='overview'> <div id='overview'>
<h1>{ship.name}</h1> <h1>{ship.name}</h1>
<div id='build'> <div id='build'>
@@ -164,22 +209,21 @@ export default class OutfittingPage extends Page {
</div> </div>
<ShipSummaryTable ship={ship} code={code} /> <ShipSummaryTable ship={ship} code={code} />
<StandardSlotSection ship={ship} code={sStr} onChange={shipUpdated} currentMenu={menu} /> <StandardSlotSection ship={ship} code={sStr} onChange={shipUpdated} currentMenu={menu} />
<InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} /> <InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} />
<HardpointsSlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} /> <HardpointsSlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} /> <UtilitySlotSection ship={ship} code={hStr} onChange={shipUpdated} currentMenu={menu} />
<PowerManagement ship={ship} code={code} onChange={shipUpdated} /> <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> <h1>{translate('jump range')}</h1>
<LineChart <LineChart
width={chartWidth}
xMax={ship.cargoCapacity} xMax={ship.cargoCapacity}
yMax={ship.unladenRange} yMax={ship.unladenRange}
xUnit={units.T} xUnit={translate('T')}
yUnit={units.LY} yUnit={translate('LY')}
yLabel={translate('jump range')} yLabel={translate('jump range')}
xLabel={translate('cargo')} xLabel={translate('cargo')}
func={state.jumpRangeChartFunc} func={state.jumpRangeChartFunc}
@@ -189,10 +233,11 @@ export default class OutfittingPage extends Page {
<div className='group third'> <div className='group third'>
<h1>{translate('total range')}</h1> <h1>{translate('total range')}</h1>
<LineChart <LineChart
width={chartWidth}
xMax={ship.cargoCapacity} xMax={ship.cargoCapacity}
yMax={ship.boostSpeed} yMax={ship.unladenTotalRange}
xUnit={units.T} xUnit={translate('T')}
yUnit={units.ms} yUnit={translate('LY')}
yLabel={translate('total range')} yLabel={translate('total range')}
xLabel={translate('cargo')} xLabel={translate('cargo')}
func={state.totalRangeChartFunc} func={state.totalRangeChartFunc}
@@ -202,25 +247,30 @@ export default class OutfittingPage extends Page {
<div className='group third'> <div className='group third'>
<h1>{translate('speed')}</h1> <h1>{translate('speed')}</h1>
<LineChart <LineChart
width={chartWidth}
xMax={ship.cargoCapacity} xMax={ship.cargoCapacity}
yMax={ship.boostSpeed} yMax={ship.topBoost + 10}
xUnit={units.T} xUnit={translate('T')}
yUnit={units.ms} yUnit={translate('m/s')}
yLabel={translate('speed')} yLabel={translate('speed')}
series={SPEED_SERIES} series={SPEED_SERIES}
color={SPEED_COLORS} colors={SPEED_COLORS}
xLabel={translate('cargo')} xLabel={translate('cargo')}
func={state.speedChartFunc} func={state.speedChartFunc}
/> />
</div> </div>
{/* <div className='group half'>
<div class='group half'> <table style={{ width: '100%', lineHeight: '1em'}}>
<div slider max='ship.fuelCapacity' unit=''T'' on-change='::fuelChange(val)' style='position:relative; margin: 0 auto;'> <tbody >
<svg class='icon xl primary-disabled' style='position:absolute;height: 100%;'><use xlink:href='#fuel'></use></svg> <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>
</div>
*/}
</div> </div>
); );

View File

@@ -1,17 +1,25 @@
import React from 'react'; import React from 'react';
import shallowEqual from '../utils/shallowEqual'; import shallowEqual from '../utils/shallowEqual';
/**
* @class Abstract Page
*/
export default class Page extends React.Component { export default class Page extends React.Component {
static contextTypes = { static contextTypes = {
route: React.PropTypes.object.isRequired, route: React.PropTypes.object.isRequired,
language: React.PropTypes.object.isRequired language: React.PropTypes.object.isRequired,
sizeRatio: React.PropTypes.number.isRequired
}; };
static propTypes = { static propTypes = {
currentMenu: React.PropTypes.any currentMenu: React.PropTypes.any
}; };
/**
* Created an instance of a Page. This is an abstract class.
* @param {object} props Properties
*/
constructor(props) { constructor(props) {
super(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) { shouldComponentUpdate(nextProps, nextState, nextContext) {
return !shallowEqual(this.props, nextProps) return !shallowEqual(this.props, nextProps)
|| !shallowEqual(this.state, nextState) || !shallowEqual(this.state, nextState)
|| !shallowEqual(this.context, nextContext) || !shallowEqual(this.context, nextContext)
} }
/**
* Update the window title upon mount
*/
componentWillMount() { componentWillMount() {
document.title = this.state.title || 'Coriolis'; document.title = this.state.title || 'Coriolis';
} }
/**
* Updates the title upon change
* @param {Object} newProps Incoming properties
* @param {Object} newState Incoming state
*/
componentWillUpdate(newProps, newState) { componentWillUpdate(newProps, newState) {
document.title = newState.title || 'Coriolis'; document.title = newState.title || 'Coriolis';
} }

View File

@@ -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'><Link href={'/outfit/' + s.id}>{s.name}</Link></td>
<td className='le'>{s.manufacturer}</td> <td className='le'>{s.manufacturer}</td>
<td className='cap'>{translate(SizeMap[s.class])}</td> <td className='cap'>{translate(SizeMap[s.class])}</td>
<td className='ri'>{fInt(s.speed)}{u.ms}</td> <td>{s.agility}</td>
<td className='ri'>{fInt(s.boost)}{u.ms}</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'>{s.baseArmour}</td>
<td className='ri'>{fInt(s.baseShieldStrength)}{u.MJ}</td> <td className='ri'>{fInt(s.baseShieldStrength)}{u.MJ}</td>
<td className='ri'>{fInt(s.topSpeed)}{u.ms}</td> <td className='ri'>{fInt(s.topSpeed)}{u['m/s']}</td>
<td className='ri'>{fInt(s.topBoost)}{u.ms}</td> <td className='ri'>{fInt(s.topBoost)}{u['m/s']}</td>
<td className='ri'>{fRound(s.maxJumpRange)}{u.LY}</td> <td className='ri'>{fRound(s.maxJumpRange)}{u.LY}</td>
<td className='ri'>{fInt(s.maxCargo)}{u.T}</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[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 le' onClick={sortShips('name')}>{translate('ship')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th> <th rowSpan={2} className='sortable' onClick={sortShips('manufacturer')}>{translate('manufacturer')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th> <th rowSpan={2} className='sortable' onClick={sortShips('class')}>{translate('size')}</th>
<th rowSpan={2} className='sortable' onClick={sortShips('agility')}>{translate('agility')}</th>
<th colSpan={4}>{translate('base')}</th> <th colSpan={4}>{translate('base')}</th>
<th colSpan={4}>{translate('max')}</th> <th colSpan={4}>{translate('max')}</th>
<th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th> <th colSpan={5} className='sortable' onClick={sortShips('hpCount')}>{translate('hardpoints')}</th>

View File

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

View File

@@ -94,80 +94,78 @@ export const ShipFacets = [
{ // 0 { // 0
title: 'agility', title: 'agility',
props: ['agility'], props: ['agility'],
unit: '', fmt: 'int'
fmt: 'fCrd'
}, },
{ // 1 { // 1
title: 'speed', title: 'speed',
props: ['topSpeed', 'topBoost'], props: ['topSpeed', 'topBoost'],
lbls: ['thrusters', 'boost'], lbls: ['thrusters', 'boost'],
unit: 'm/s', unit: 'm/s',
fmt: 'fCrd' fmt: 'int'
}, },
{ // 2 { // 2
title: 'armour', title: 'armour',
props: ['armour'], props: ['armour'],
unit: '', unit: '',
fmt: 'fCrd' fmt: 'int'
}, },
{ // 3 { // 3
title: 'shields', title: 'shields',
props: ['shieldStrength'], props: ['shieldStrength'],
unit: 'MJ', unit: 'MJ',
fmt: 'fRound' fmt: 'round'
}, },
{ // 4 { // 4
title: 'jump range', title: 'jump range',
props: ['unladenRange', 'fullTankRange', 'ladenRange'], props: ['unladenRange', 'fullTankRange', 'ladenRange'],
lbls: ['max', 'full tank', 'laden'], lbls: ['max', 'full tank', 'laden'],
unit: 'LY', unit: 'LY',
fmt: 'fRound' fmt: 'round'
}, },
{ // 5 { // 5
title: 'mass', title: 'mass',
props: ['unladenMass', 'ladenMass'], props: ['unladenMass', 'ladenMass'],
lbls: ['unladen', 'laden'], lbls: ['unladen', 'laden'],
unit: 'T', unit: 'T',
fmt: 'fRound' fmt: 'round'
}, },
{ // 6 { // 6
title: 'cargo', title: 'cargo',
props: ['cargoCapacity'], props: ['cargoCapacity'],
unit: 'T', unit: 'T',
fmt: 'fRound' fmt: 'round'
}, },
{ // 7 { // 7
title: 'fuel', title: 'fuel',
props: ['fuelCapacity'], props: ['fuelCapacity'],
unit: 'T', unit: 'T',
fmt: 'fRound' fmt: 'int'
}, },
{ // 8 { // 8
title: 'power', title: 'power',
props: ['powerRetracted', 'powerDeployed', 'powerAvailable'], props: ['powerRetracted', 'powerDeployed', 'powerAvailable'],
lbls: ['retracted', 'deployed', 'available'], lbls: ['retracted', 'deployed', 'available'],
unit: 'MW', unit: 'MW',
fmt: 'fPwr' fmt: 'f2'
}, },
{ // 9 { // 9
title: 'cost', title: 'cost',
props: ['totalCost'], props: ['totalCost'],
unit: 'CR', unit: 'CR',
fmt: 'fCrd' fmt: 'int'
}, },
{ // 10 { // 10
title: 'total range', title: 'total range',
props: ['unladenTotalRange', 'ladenTotalRange'], props: ['unladenTotalRange', 'ladenTotalRange'],
lbls: ['unladen', 'laden'], lbls: ['unladen', 'laden'],
unit: 'LY', unit: 'LY',
fmt: 'fRound' fmt: 'round'
}, },
{ // 11 { // 11
title: 'DPS', title: 'DPS',
props: ['totalDps'], props: ['totalDps'],
lbls: ['DPS'], lbls: ['DPS'],
unit: '', fmt: 'round'
fmt: 'fRound'
} }
]; ];
@@ -185,8 +183,10 @@ export const Insurance = {
*/ */
export const Discounts = { export const Discounts = {
'0%': 1, '0%': 1,
'2.5%': 0.975,
'5%': 0.95, '5%': 0.95,
'10%': 0.90, '10%': 0.90,
'12.5%': 0.875,
'15%': 0.85, '15%': 0.85,
'20%': 0.80, '20%': 0.80,
'25%': 0.75 '25%': 0.75

View File

@@ -165,7 +165,7 @@ export function findHardpointId(groupName, clss, rating, name, mount, missile) {
*/ */
export function bulkheads(shipId, index) { export function bulkheads(shipId, index) {
let bulkhead = Ships[shipId].bulkheads[index]; let bulkhead = Ships[shipId].bulkheads[index];
bulkhead.class = 8; bulkhead.class = 1;
bulkhead.rating = 'I'; bulkhead.rating = 'I';
bulkhead.name = BulkheadNames[index] bulkhead.name = BulkheadNames[index]

View File

@@ -38,7 +38,7 @@ export function toDetailedBuild(buildName, ship, code) {
internal = ship.internal; internal = ship.internal;
var data = { var data = {
$schema: 'http://cdn.coriolis.io/schemas/ship-loadout/2.json#', $schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#',
name: buildName, name: buildName,
ship: ship.name, ship: ship.name,
references: [{ references: [{
@@ -76,7 +76,7 @@ export function toDetailedBuild(buildName, ship, code) {
}; };
export function fromDetailedBuild(detailedBuild) { 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) { if (!shipId) {
throw 'No such ship: ' + detailedBuild.ship; throw 'No such ship: ' + detailedBuild.ship;
@@ -133,9 +133,9 @@ export function toDetailedExport(builds) {
for (var shipId in builds) { for (var shipId in builds) {
for (var buildName in builds[shipId]) { for (var buildName in builds[shipId]) {
var code = builds[shipId][buildName]; var code = builds[shipId][buildName];
var shipData = ShipsDB[shipId]; var shipData = Ships[shipId];
var ship = new Ship(shipId, shipData.properties, shipData.slots); var ship = new Ship(shipId, shipData.properties, shipData.slots);
toShip(ship, code); ship.buildFrom(code);
data.push(toDetailedBuild(buildName, ship, code)); data.push(toDetailedBuild(buildName, ship, code));
} }
} }

View File

@@ -149,8 +149,16 @@ export default class Ship {
* @param {number} fuel Fuel available in tons * @param {number} fuel Fuel available in tons
* @return {number} Jump range in Light Years * @return {number} Jump range in Light Years
*/ */
getJumpRangeForMass(mass, fuel) { getJumpRangeWith(fuel, cargo) {
return Calc.jumpRange(mass, this.standard[2].m, fuel); 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);
} }
/** /**

View File

@@ -88,10 +88,8 @@ class Persist extends EventEmitter {
let newBuild = !this.builds[shipId][name]; let newBuild = !this.builds[shipId][name];
this.builds[shipId][name] = code; this.builds[shipId][name] = code;
_put(LS_KEY_BUILDS, this.builds); _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_BUILDS] = this.getBuilds();
data[LS_KEY_COMPARISONS] = this.getComparisons(); data[LS_KEY_COMPARISONS] = this.getComparisons();
data[LS_KEY_INSURANCE] = this.getInsurance(); data[LS_KEY_INSURANCE] = this.getInsurance();
data[LS_KEY_DISCOUNTS] = this.getDiscount(); data[LS_KEY_DISCOUNTS] = this.discounts;
return data; return data;
}; };

View File

@@ -1,9 +1,10 @@
/** /**
* [slotName description] * Returns the translate name for the module mounted in the specified
* @param {[type]} translate [description] * slot.
* @param {[type]} slot [description] * @param {function} translate Translation function
* @return {[type]} [description] * @param {object} slot Slot object
* @return {string} The translated name
*/ */
export function slotName(translate, slot) { export function slotName(translate, slot) {
return slot.m ? translate(slot.m.name || slot.m.grp) : ''; 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 * @return {function} Comparator function for slot names
*/ */
export function nameComparator(translate) { 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));
};
} }

View File

@@ -13,7 +13,6 @@
@import 'select'; @import 'select';
@import 'modal'; @import 'modal';
@import 'charts'; @import 'charts';
@import 'slider';
@import 'chart-tooltip'; @import 'chart-tooltip';
@import 'buttons'; @import 'buttons';
@import 'error'; @import 'error';
@@ -43,7 +42,7 @@ div, a, li {
#coriolis { #coriolis {
width: 100%; width: 100%;
min-height: 100%; min-height: 95%;
} }
.page { .page {

View File

@@ -54,9 +54,10 @@
display: block; display: block;
.user-select-none(); .user-select-none();
cursor: default; cursor: default;
white-space: nowrap;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
padding: 0; padding: 0 1px;
.as-sortable-placeholder { .as-sortable-placeholder {
background-color: @primary-bg; background-color: @primary-bg;
@@ -66,23 +67,23 @@
} }
} }
.facet-placeholder {
display: inline-block;
padding: 0.5em 0.5em;
background-color: @primary-bg;
}
.facet { .facet {
cursor: pointer; cursor: pointer;
display: inline-block;
background-color: @primary-bg; background-color: @primary-bg;
margin: 0; margin: 0;
padding: 0.5em 0.5em; padding: 0 0.5em;
line-height: 2.5em;
list-style: none; list-style: none;
white-space: nowrap; white-space: nowrap;
color: @disabled; color: @disabled;
svg {
fill: @disabled;
}
.move {
cursor: ew-resize;
}
&.active { &.active {
color: @warning; color: @warning;
svg { svg {

View File

@@ -114,12 +114,6 @@
}); });
} }
.sortable {
&:hover {
color: @primary;
}
}
.shorten { .shorten {
overflow: hidden; overflow: hidden;
max-width: 8em; max-width: 8em;
@@ -225,8 +219,7 @@
} }
svg g { .threshold {
.threshold {
stroke: @secondary-disabled !important; stroke: @secondary-disabled !important;
fill: @secondary-disabled !important; fill: @secondary-disabled !important;
@@ -234,7 +227,6 @@ svg g {
stroke: @warning !important; stroke: @warning !important;
fill: @warning !important; fill: @warning !important;
} }
}
} }
#componentPriority { #componentPriority {

View File

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

View File

@@ -1,31 +1,8 @@
.sortable { .sortable {
.user-select-none(); .user-select-none();
cursor: pointer; cursor: pointer;
}
.as-sortable-item, .as-sortable-placeholder { &:hover {
display: inline-block; color: @primary;
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;
} }

View File

@@ -30,13 +30,6 @@ thead {
font-weight: normal; font-weight: normal;
padding: 2px 0.4em 0; // Padding top for font vertical alignment padding: 2px 0.4em 0; // Padding top for font vertical alignment
&.prop {
cursor: pointer;
&:hover {
color: @primary;
}
}
&.lft { &.lft {
border-left: 1px solid @primary-bg; border-left: 1px solid @primary-bg;
} }

View File

@@ -302,7 +302,7 @@
} }
}, },
"definitions": { "definitions": {
"standardRatings": { "enum": ["A", "B", "C", "D", "E"] }, "standardRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H"] },
"allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "I" ] } "allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H", "I" ] }
} }
} }

View 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" ] }
}
}