Make various components stateless

This commit is contained in:
Cmdr McDonald
2017-03-20 13:52:24 +00:00
parent 2f5d123f02
commit 73a75c69a3
12 changed files with 286 additions and 326 deletions

View File

@@ -320,7 +320,6 @@
"shieldExplRes": 0.5,
"shieldKinRes": 0.4,
"shieldThermRes": -0.2,
"timeToDrain": 7.04,
"crew": 3
}
}

View File

@@ -76,6 +76,7 @@
"json-loader": "^0.5.3",
"less": "^2.5.3",
"less-loader": "^2.2.1",
"react-addons-perf": "^15.4.2",
"react-addons-test-utils": "^15.0.1",
"react-measure": "^1.4.6",
"react-testutils-additions": "^15.1.0",

View File

@@ -17,6 +17,7 @@ export default class Boost extends TranslatedComponent {
static propTypes = {
marker: React.PropTypes.string.isRequired,
ship: React.PropTypes.object.isRequired,
boost: React.PropTypes.bool.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -27,14 +28,10 @@ export default class Boost extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
const ship = props.ship;
const { ship, boost } = props;
this._keyDown = this._keyDown.bind(this);
this._toggleBoost = this._toggleBoost.bind(this);
this.state = {
boost: false
};
}
/**
@@ -51,25 +48,6 @@ export default class Boost extends TranslatedComponent {
document.removeEventListener('keydown', this._keyDown);
}
/**
* Update values if we change ship
* @param {Object} nextProps Incoming/Next properties
* @returns {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { boost } = this.state;
const nextShip = nextProps.ship;
const nextBoost = nextShip.canBoost() ? boost : false;
if (nextBoost != boost) {
this.setState({
boost: nextBoost
});
}
return true;
}
/**
* Handle Key Down
* @param {Event} e Keyboard Event
@@ -91,10 +69,7 @@ export default class Boost extends TranslatedComponent {
* Toggle the boost feature
*/
_toggleBoost() {
let { boost } = this.state;
boost = !boost;
this.setState({ boost });
this.props.onChange(boost);
this.props.onChange(!this.props.boost);
}
/**
@@ -103,8 +78,7 @@ export default class Boost extends TranslatedComponent {
*/
render() {
const { formats, translate, units } = this.context.language;
const { ship } = this.props;
const { boost } = this.state;
const { ship, boost } = this.props;
// TODO disable if ship cannot boost
return (

View File

@@ -9,7 +9,8 @@ import Slider from '../components/Slider';
*/
export default class Cargo extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
cargo: React.PropTypes.number.isRequired,
cargoCapacity: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -21,35 +22,7 @@ export default class Cargo extends TranslatedComponent {
constructor(props, context) {
super(props);
const ship = this.props.ship;
this.state = {
cargoCapacity: ship.cargoCapacity,
cargoLevel: 0,
};
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { cargoLevel, cargoCapacity } = this.state;
const nextCargoCapacity = nextProps.ship.cargoCapacity;
if (nextCargoCapacity != cargoCapacity) {
// We keep the absolute cargo amount the same if possible so recalculate the relative level
const nextCargoLevel = Math.min((cargoLevel * cargoCapacity) / nextCargoCapacity, 1);
this.setState({ cargoLevel: nextCargoLevel, cargoCapacity: nextCargoCapacity });
// Notify if appropriate
if (nextCargoLevel * nextCargoCapacity != cargoLevel * cargoCapacity) {
this.props.onChange(Math.round(nextCargoLevel * nextCargoCapacity));
}
}
return true;
this._cargoChange = this._cargoChange.bind(this);
}
/**
@@ -57,14 +30,12 @@ export default class Cargo extends TranslatedComponent {
* @param {number} cargoLevel percentage level from 0 to 1
*/
_cargoChange(cargoLevel) {
const { cargoCapacity } = this.state;
const { cargo, cargoCapacity } = this.props;
if (cargoCapacity > 0) {
// We round the cargo level to a suitable value given the capacity
cargoLevel = Math.round(cargoLevel * cargoCapacity) / cargoCapacity;
if (cargoLevel != this.state.cargoLevel) {
this.setState({ cargoLevel });
this.props.onChange(Math.round(cargoLevel * cargoCapacity));
// We round the cargo to whole number of tonnes
const newCargo = Math.round(cargoLevel * cargoCapacity);
if (newCargo != cargo) {
this.props.onChange(newCargo);
}
}
}
@@ -76,20 +47,20 @@ export default class Cargo extends TranslatedComponent {
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { cargoLevel, cargoCapacity } = this.state;
const { cargo, cargoCapacity } = this.props;
return (
<span>
<h3>{translate('cargo carried')}: {formats.int(cargoLevel * cargoCapacity)}{units.T}</h3>
<h3>{translate('cargo carried')}: {formats.int(cargo)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._cargoChange.bind(this)}
onChange={this._cargoChange}
axisUnit={translate('T')}
percent={cargoLevel}
percent={cargo / cargoCapacity}
max={cargoCapacity}
scale={sizeRatio}
onResize={onWindowResize}

View File

@@ -7,9 +7,10 @@ import Slider from '../components/Slider';
* Engagement range slider
* Requires an onChange() function of the form onChange(range), providing the range in metres, which is triggered on range change
*/
export default class Range extends TranslatedComponent {
export default class EngagementRange extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
engagementRange: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -21,47 +22,15 @@ export default class Range extends TranslatedComponent {
constructor(props, context) {
super(props);
const ship = this.props.ship;
const { ship } = props;
const maxRange = this._calcMaxRange(ship);
this.state = {
maxRange,
rangeLevel: 1000 / maxRange,
maxRange
};
}
/**
*
*/
componentWillMount() {
// Pass initial state
this.props.onChange(this.state.maxRange * this.state.rangeLevel);
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { rangeLevel, maxRange } = this.state;
const nextMaxRange = this._calcMaxRange(nextProps.ship);
if (nextMaxRange != maxRange) {
// We keep the absolute range amount the same if possible so recalculate the relative level
const nextRangeLevel = Math.min((rangeLevel * maxRange) / nextMaxRange, 1);
this.setState({ rangeLevel: nextRangeLevel, maxRange: nextMaxRange });
// Notify if appropriate
if (nextRangeLevel * nextMaxRange != rangeLevel * maxRange) {
this.props.onChange(Math.round(nextRangeLevel * nextMaxRange));
}
}
return true;
}
/**
* Calculate the maximum range of a ship's weapons
* @param {Object} ship The ship
@@ -87,12 +56,12 @@ export default class Range extends TranslatedComponent {
*/
_rangeChange(rangeLevel) {
const { maxRange } = this.state;
// We round the range to an integer value
rangeLevel = Math.round(rangeLevel * maxRange) / maxRange;
if (rangeLevel != this.state.rangeLevel) {
this.setState({ rangeLevel });
this.props.onChange(Math.round(rangeLevel * maxRange));
// We round the range to an integer value
const range = Math.round(rangeLevel * maxRange);
if (range !== this.props.engagementRange) {
this.props.onChange(range);
}
}
@@ -103,11 +72,12 @@ export default class Range extends TranslatedComponent {
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { rangeLevel, maxRange } = this.state;
const { engagementRange } = this.props;
const { maxRange } = this.state;
return (
<span>
<h3>{translate('engagement range')}: {formats.int(rangeLevel * maxRange)}{translate('m')}</h3>
<h3>{translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
@@ -116,7 +86,7 @@ export default class Range extends TranslatedComponent {
axis={true}
onChange={this._rangeChange.bind(this)}
axisUnit={translate('m')}
percent={rangeLevel}
percent={engagementRange / maxRange}
max={maxRange}
scale={sizeRatio}
onResize={onWindowResize}

View File

@@ -9,7 +9,8 @@ import Slider from '../components/Slider';
*/
export default class Fuel extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
fuel: React.PropTypes.number.isRequired,
fuelCapacity: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -21,35 +22,7 @@ export default class Fuel extends TranslatedComponent {
constructor(props, context) {
super(props);
const ship = this.props.ship;
this.state = {
fuelCapacity: ship.fuelCapacity,
fuelLevel: 1,
};
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { fuelLevel, fuelCapacity } = this.state;
const nextFuelCapacity = nextProps.ship.fuelCapacity;
if (nextFuelCapacity != fuelCapacity) {
// We keep the absolute fuel amount the same if possible so recalculate the relative level
const nextFuelLevel = Math.min((fuelLevel * fuelCapacity) / nextFuelCapacity, 1);
this.setState({ fuelLevel: nextFuelLevel, fuelCapacity: nextFuelCapacity });
// Notify if appropriate
if (nextFuelLevel * nextFuelCapacity != fuelLevel * fuelCapacity) {
this.props.onChange(nextFuelLevel * nextFuelCapacity);
}
}
return true;
this._fuelChange = this._fuelChange.bind(this);
}
/**
@@ -57,8 +30,13 @@ export default class Fuel extends TranslatedComponent {
* @param {number} fuelLevel percentage level from 0 to 1
*/
_fuelChange(fuelLevel) {
this.setState({ fuelLevel });
this.props.onChange(fuelLevel * this.state.fuelCapacity);
const { fuel, fuelCapacity } = this.props;
const newFuel = fuelLevel * fuelCapacity;
// Only send an update if the fuel has changed significantly
if (Math.round(fuel * 10) != Math.round(newFuel * 10)) {
this.props.onChange(Math.round(newFuel * 10) / 10);
}
}
/**
@@ -68,20 +46,20 @@ export default class Fuel extends TranslatedComponent {
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { fuelLevel, fuelCapacity } = this.state;
const { fuel, fuelCapacity } = this.props;
return (
<span>
<h3>{translate('fuel carried')}: {formats.f2(fuelLevel * fuelCapacity)}{units.T}</h3>
<h3>{translate('fuel carried')}: {formats.f1(fuel)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._fuelChange.bind(this)}
onChange={this._fuelChange}
axisUnit={translate('T')}
percent={fuelLevel}
percent={fuel / fuelCapacity}
max={fuelCapacity}
scale={sizeRatio}
onResize={onWindowResize}

View File

@@ -66,9 +66,12 @@ export default class OutfittingSubpages extends TranslatedComponent {
let { ship, buildName, code, onChange } = this.props;
Persist.setOutfittingTab('power');
const powerMarker = `${ship.toString()}`;
const costMarker = `${ship.toString().split('.')[0]}`;
return <div>
<PowerManagement ship={ship} code={code} onChange={onChange} />
<CostSection ship={ship} buildName={buildName} code={code} />
<PowerManagement ship={ship} code={powerMarker} onChange={onChange} />
<CostSection ship={ship} buildName={buildName} code={costMarker} />
</div>;
}

View File

@@ -15,7 +15,9 @@ import Module from '../shipyard/Module';
*/
export default class Pips extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
sys: React.PropTypes.number.isRequired,
eng: React.PropTypes.number.isRequired,
wep: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -26,24 +28,9 @@ export default class Pips extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
const ship = props.ship;
const pd = ship.standard[4].m;
const { sys, eng, wep } = props;
this._keyDown = this._keyDown.bind(this);
let pipsSvg = this._renderPips(2, 2, 2);
this.state = {
sys: 2,
eng: 2,
wep: 2,
sysCap: pd.getSystemsCapacity(),
engCap: pd.getEnginesCapacity(),
wepCap: pd.getWeaponsCapacity(),
sysRate: pd.getSystemsRechargeRate(),
engRate: pd.getEnginesRechargeRate(),
wepRate: pd.getWeaponsRechargeRate(),
pipsSvg
};
}
/**
@@ -60,41 +47,6 @@ export default class Pips extends TranslatedComponent {
document.removeEventListener('keydown', this._keyDown);
}
/**
* Update values if we change ship
* @param {Object} nextProps Incoming/Next properties
* @returns {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { sysCap, engCap, wepCap, sysRate, engRate, wepRate } = this.state;
const nextShip = nextProps.ship;
const pd = nextShip.standard[4].m;
const nextSysCap = pd.getSystemsCapacity();
const nextEngCap = pd.getEnginesCapacity();
const nextWepCap = pd.getWeaponsCapacity();
const nextSysRate = pd.getSystemsRechargeRate();
const nextEngRate = pd.getEnginesRechargeRate();
const nextWepRate = pd.getWeaponsRechargeRate();
if (nextSysCap != sysCap ||
nextEngCap != engCap ||
nextWepCap != wepCap ||
nextSysRate != sysRate ||
nextEngRate != engRate ||
nextWepRate != wepRate) {
this.setState({
sysCap: nextSysCap,
engCap: nextEngCap,
wepCap: nextWepCap,
sysRate: nextSysRate,
engRate: nextEngRate,
wepRate: nextWepRate
});
}
return true;
}
/**
* Handle Key Down
* @param {Event} e Keyboard Event
@@ -142,10 +94,9 @@ export default class Pips extends TranslatedComponent {
* Reset the capacitor
*/
_reset() {
let { sys, eng, wep } = this.state;
let { sys, eng, wep } = this.props;
if (sys != 2 || eng != 2 || wep != 2) {
sys = eng = wep = 2;
this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) });
this.props.onChange(sys, eng, wep);
}
}
@@ -154,7 +105,7 @@ export default class Pips extends TranslatedComponent {
* Increment the SYS capacitor
*/
_incSys() {
let { sys, eng, wep } = this.state;
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - sys);
if (required > 0) {
@@ -181,7 +132,6 @@ export default class Pips extends TranslatedComponent {
sys += 1;
}
}
this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) });
this.props.onChange(sys, eng, wep);
}
}
@@ -190,7 +140,7 @@ export default class Pips extends TranslatedComponent {
* Increment the ENG capacitor
*/
_incEng() {
let { sys, eng, wep } = this.state;
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - eng);
if (required > 0) {
@@ -217,7 +167,6 @@ export default class Pips extends TranslatedComponent {
eng += 1;
}
}
this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) });
this.props.onChange(sys, eng, wep);
}
}
@@ -226,7 +175,7 @@ export default class Pips extends TranslatedComponent {
* Increment the WEP capacitor
*/
_incWep() {
let { sys, eng, wep } = this.state;
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - wep);
if (required > 0) {
@@ -253,7 +202,6 @@ export default class Pips extends TranslatedComponent {
wep += 1;
}
}
this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) });
this.props.onChange(sys, eng, wep);
}
}
@@ -313,14 +261,14 @@ export default class Pips extends TranslatedComponent {
*/
render() {
const { formats, translate, units } = this.context.language;
const { ship } = this.props;
const { sys, eng, wep, sysCap, engCap, wepCap, sysRate, engRate, wepRate, pipsSvg } = this.state;
const { sys, eng, wep } = this.props;
const onSysClicked = this.onClick.bind(this, 'SYS');
const onEngClicked = this.onClick.bind(this, 'ENG');
const onWepClicked = this.onClick.bind(this, 'WEP');
const onRstClicked = this.onClick.bind(this, 'RST');
const pipsSvg = this._renderPips(sys, eng, wep);
return (
<span id='pips'>
<table>
@@ -349,15 +297,3 @@ export default class Pips extends TranslatedComponent {
);
}
}
// <tr>
// <td>{translate('capacity')} ({units.MJ})</td>
// <td>{formats.f1(sysCap)}</td>
// <td>{formats.f1(engCap)}</td>
// <td>{formats.f1(wepCap)}</td>
// </tr>
// <tr>
// <td>{translate('recharge')} ({units.MW})</td>
// <td>{formats.f1(sysRate * (sys / 4))}</td>
// <td>{formats.f1(engRate * (eng / 4))}</td>
// <td>{formats.f1(wepRate * (wep / 4))}</td>
// </tr>

View File

@@ -13,12 +13,12 @@ import cn from 'classnames';
export default class ShipPicker extends TranslatedComponent {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
ship: React.PropTypes.object,
ship: React.PropTypes.string.isRequired,
build: React.PropTypes.string
};
static defaultProps = {
ship: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults)
ship: 'eagle'
}
/**
@@ -33,44 +33,21 @@ export default class ShipPicker extends TranslatedComponent {
this._toggleMenu = this._toggleMenu.bind(this);
this._closeMenu = this._closeMenu.bind(this);
this.state = {
ship: props.ship,
build: props.build
};
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
const { ship, build } = this.state;
const { nextShip, nextBuild } = nextProps;
if (nextShip != undefined && nextShip != ship && nextBuild != build) {
this.setState({ ship: nextShip, build: nextBuild });
}
return true;
this.state = { menuOpen: false };
}
/**
* Update ship
* @param {object} shipId the ship
* @param {object} ship the ship
* @param {string} build the build, if present
*/
_shipChange(shipId, build) {
const ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
if (build) {
// Ship is a particular build
ship.buildFrom(Persist.getBuild(shipId, build));
} else {
// Ship is a stock build
ship.buildWith(Ships[shipId].defaults);
}
_shipChange(ship, build) {
this._closeMenu();
this.setState({ ship, build });
this.props.onChange(ship, build);
// Ensure that the ship has changed
if (ship !== this.props.ship || this.build !== this.props.build) {
this.props.onChange(ship, build);
}
}
/**
@@ -78,7 +55,7 @@ export default class ShipPicker extends TranslatedComponent {
* @returns {object} the picker menu
*/
_renderPickerMenu() {
const { ship, build } = this.state;
const { ship, build } = this.props;
const _shipChange = this._shipChange;
const builds = Persist.getBuilds();
@@ -86,7 +63,7 @@ export default class ShipPicker extends TranslatedComponent {
for (let shipId of this.shipOrder) {
const shipBuilds = [];
// Add stock build
const stockSelected = (ship.id == shipId && !build);
const stockSelected = (ship == shipId && !build);
shipBuilds.push(<li key={shipId} className={ cn({ 'selected': stockSelected })} onClick={_shipChange.bind(this, shipId, null)}>Stock</li>);
if (builds[shipId]) {
let buildNameOrder = Object.keys(builds[shipId]).sort();
@@ -126,9 +103,10 @@ export default class ShipPicker extends TranslatedComponent {
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { menuOpen, ship, build } = this.state;
const { ship, build } = this.props;
const { menuOpen } = this.state;
const shipString = ship.name + ': ' + (build ? build : translate('stock'));
const shipString = ship + ': ' + (build ? build : translate('stock'));
return (
<div className='shippicker' onClick={ (e) => e.stopPropagation() }>
<div className='menu'>

View File

@@ -30,24 +30,20 @@ export default class ShipSummaryTable extends TranslatedComponent {
let u = language.units;
let formats = language.formats;
let { time, int, round, f1, f2 } = formats;
let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') });
let sgRecover = '-';
let sgRecharge = '-';
let hide = tooltip.bind(null, null);
if (ship.shield) {
sgRecover = time(ship.calcShieldRecovery());
sgRecharge = time(ship.calcShieldRecharge());
}
const shieldGenerator = ship.findInternalByGroup('sg');
const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator });
const timeToDrain = Calc.timeToDrainWep(ship, wep);
const canThrust = ship.canThrust();
const canBoost = ship.canBoost();
return <div id='summary'>
<table id='summaryTable'>
<thead>
<tr className='main'>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
<th onMouseEnter={termtip.bind(null, 'damage per second')} onMouseLeave={hide} rowSpan={2}>{translate('DPS')}</th>
<th onMouseEnter={termtip.bind(null, 'energy per second')} onMouseLeave={hide} rowSpan={2}>{translate('EPS')}</th>
<th onMouseEnter={termtip.bind(null, 'time to drain WEP capacitor')} onMouseLeave={hide} rowSpan={2}>{translate('TTD')}</th>
@@ -72,8 +68,8 @@ export default class ShipSummaryTable extends TranslatedComponent {
</thead>
<tbody>
<tr>
<td>{ ship.canThrust() ? <span>{int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ ship.canBoost() ? <span>{int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ canThrust ? <span>{int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ canBoost ? <span>{int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{f1(ship.totalDps)}</td>
<td>{f1(ship.totalEps)}</td>
<td>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>

View File

@@ -1,14 +1,17 @@
import React from 'react';
// import Perf from 'react-addons-perf';
import { findDOMNode } from 'react-dom';
import { Ships } from 'coriolis-data/dist';
import cn from 'classnames';
import Page from './Page';
import Router from '../Router';
import Persist from '../stores/Persist';
import * as Utils from '../utils/UtilityFunctions';
import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer';
import { outfitURL } from '../utils/UrlGenerators';
import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons';
import LZString from 'lz-string';
import ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection';
@@ -46,7 +49,8 @@ export default class OutfittingPage extends Page {
*/
constructor(props, context) {
super(props, context);
this.state = this._initState(context);
// window.Perf = Perf;
this.state = this._initState(props, context);
this._keyDown = this._keyDown.bind(this);
this._exportBuild = this._exportBuild.bind(this);
this._pipsUpdated = this._pipsUpdated.bind(this);
@@ -59,10 +63,11 @@ export default class OutfittingPage extends Page {
/**
* [Re]Create initial state from context
* @param {Object} props React component properties
* @param {context} context React component context
* @return {Object} New state object
*/
_initState(context) {
_initState(props, context) {
let params = context.route.params;
let shipId = params.ship;
let code = params.code;
@@ -84,6 +89,8 @@ export default class OutfittingPage extends Page {
this._getTitle = getTitle.bind(this, data.properties.name);
// Obtain ship control from code
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
return {
error: null,
title: this._getTitle(buildName),
@@ -94,14 +101,15 @@ export default class OutfittingPage extends Page {
ship,
code,
savedCode,
sys: 2,
eng: 2,
wep: 2,
fuel: ship.fuelCapacity,
cargo: 0,
boost: false,
engagementRange: 1000,
opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults)
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
};
}
@@ -123,6 +131,76 @@ export default class OutfittingPage extends Page {
this.setState(stateChanges);
}
/**
* Update the control part of the route
*/
_updateRouteOnControlChange() {
const { ship, shipId, buildName } = this.state;
const code = this._fullCode(ship);
this._updateRoute(shipId, buildName, code);
this.setState({ code });
}
/**
* Provide a full code for this ship, including any additions due to the outfitting page
* @param {Object} ship the ship
* @param {number} fuel the fuel carried by the ship (if different from that in state)
* @param {number} cargo the cargo carried by the ship (if different from that in state)
* @returns {string} the code for this ship
*/
_fullCode(ship, fuel, cargo) {
return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`;
}
/**
* Obtain the control information from the build code
* @param {Object} ship The ship
* @param {string} code The build code
* @returns {Object} The control information
*/
_obtainControlFromCode(ship, code) {
// Defaults
let sys = 2;
let eng = 2;
let wep = 2;
let boost = false;
let fuel = ship.fuelCapacity;
let cargo = ship.cargoCapacity;
let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults);
let opponentBuild = undefined;
let engagementRange = 1000;
// Obtain updates from code, if available
if (code) {
const parts = code.split('.');
if (parts.length >= 5) {
// We have control information in the code
const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/');
sys = parseFloat(control[0]);
eng = parseFloat(control[1]);
wep = parseFloat(control[2]);
boost = control[3] == 1 ? true : false;
fuel = parseFloat(control[4]);
cargo = parseInt(control[5]);
if (control[6]) {
const shipId = control[6];
opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
if (control[7] && Persist.getBuild(shipId, control[7])) {
// Ship is a particular build
opponent.buildFrom(Persist.getBuild(shipId, control[7]));
opponentBuild = control[7];
} else {
// Ship is a stock build
opponent.buildWith(Ships[shipId].defaults);
}
}
engagementRange = parseInt(control[8]);
}
}
return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange };
}
/**
* Triggered when pips have been updated
* @param {number} sys SYS pips
@@ -130,7 +208,7 @@ export default class OutfittingPage extends Page {
* @param {number} wep WEP pips
*/
_pipsUpdated(sys, eng, wep) {
this.setState({ sys, eng, wep });
this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange());
}
/**
@@ -138,7 +216,7 @@ export default class OutfittingPage extends Page {
* @param {boolean} boost true if boosting
*/
_boostUpdated(boost) {
this.setState({ boost });
this.setState({ boost }, () => this._updateRouteOnControlChange());
}
/**
@@ -146,7 +224,7 @@ export default class OutfittingPage extends Page {
* @param {number} fuel the amount of fuel, in T
*/
_fuelUpdated(fuel) {
this.setState({ fuel });
this.setState({ fuel }, () => this._updateRouteOnControlChange());
}
/**
@@ -154,7 +232,7 @@ export default class OutfittingPage extends Page {
* @param {number} cargo the amount of cargo, in T
*/
_cargoUpdated(cargo) {
this.setState({ cargo });
this.setState({ cargo }, () => this._updateRouteOnControlChange());
}
/**
@@ -162,24 +240,44 @@ export default class OutfittingPage extends Page {
* @param {number} engagementRange the engagement range, in m
*/
_engagementRangeUpdated(engagementRange) {
this.setState({ engagementRange });
this.setState({ engagementRange }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when target ship has been updated
* @param {object} opponent the opponent's ship
* @param {string} opponentBuild the name of the opponent's build
* @param {string} opponent the opponent's ship model
* @param {string} opponentBuild the name of the opponent's build
*/
_opponentUpdated(opponent, opponentBuild) {
this.setState({ opponent, opponentBuild });
const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots);
if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) {
// Ship is a particular build
opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild));
} else {
// Ship is a stock build
opponentShip.buildWith(Ships[opponent].defaults);
}
this.setState({ opponent: opponentShip, opponentBuild }, () => this._updateRouteOnControlChange());
}
/**
* Set the control code for this outfitting page
* @param {number} fuel the fuel carried by the ship (if different from that in state)
* @param {number} cargo the cargo carried by the ship (if different from that in state)
* @returns {string} The control code
*/
_controlCode(fuel, cargo) {
const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state;
const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`;
return code;
}
/**
* Save the current build
*/
_saveBuild() {
let code = this.state.ship.toString();
let { buildName, newBuildName, shipId } = this.state;
const { code, buildName, newBuildName, shipId } = this.state;
if (buildName === newBuildName) {
Persist.saveBuild(shipId, buildName, code);
@@ -196,9 +294,8 @@ export default class OutfittingPage extends Page {
* Rename the current build
*/
_renameBuild() {
let { buildName, newBuildName, shipId, ship } = this.state;
const { code, buildName, newBuildName, shipId, ship } = this.state;
if (buildName != newBuildName && newBuildName.length) {
let code = ship.toString();
Persist.deleteBuild(shipId, buildName);
Persist.saveBuild(shipId, newBuildName, code);
this._updateRoute(shipId, newBuildName, code);
@@ -210,16 +307,31 @@ export default class OutfittingPage extends Page {
* Reload build from last save
*/
_reloadBuild() {
this.state.ship.buildFrom(this.state.savedCode);
this._shipUpdated();
this.setState({ code: this.state.savedCode }, () => this._codeUpdated());
}
/**
* Reset build to Stock/Factory defaults
*/
_resetBuild() {
this.state.ship.buildWith(Ships[this.state.shipId].defaults);
this._shipUpdated();
const { ship, shipId, buildName } = this.state;
// Rebuild ship
ship.buildWith(Ships[shipId].defaults);
// Reset controls
const code = ship.toString();
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
// Update state, and refresh the ship
this.setState({
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
}, () => this._updateRoute(shipId, buildName, code));
}
/**
@@ -244,14 +356,43 @@ export default class OutfittingPage extends Page {
}
/**
* Trigger render on ship model change
* Called when the code for the ship has been updated, to synchronise the rest of the data
*/
_codeUpdated() {
const { code, ship, shipId, buildName } = this.state;
// Rebuild ship from the code
this.state.ship.buildFrom(code);
// Obtain controls from the code
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
// Update state, and refresh the route when complete
this.setState({
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
}, () => this._updateRoute(shipId, buildName, code));
}
/**
* Called when the ship has been updated, to set the code and then update accordingly
*/
_shipUpdated() {
let { shipId, buildName, ship } = this.state;
let code = ship.toString();
this._updateRoute(shipId, buildName, code);
this.setState({ code });
let { ship, shipId, buildName, cargo, fuel } = this.state;
if (cargo > ship.cargoCapacity) {
cargo = ship.cargoCapacity;
}
if (fuel > ship.fuelCapacity) {
fuel = ship.fuelCapacity;
}
const code = this._fullCode(ship, fuel, cargo);
this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code));
}
/**
@@ -271,7 +412,7 @@ export default class OutfittingPage extends Page {
*/
componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextContext));
this.setState(this._initState(nextProps, nextContext));
}
}
@@ -340,16 +481,23 @@ export default class OutfittingPage extends Page {
shipUpdated = this._shipUpdated,
canSave = (newBuildName || buildName) && code !== savedCode,
canRename = buildName && newBuildName && buildName != newBuildName,
canReload = savedCode && canSave,
hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(),
iStr = ship.getInternalString() + '.' + ship.getModificationsString();
canReload = savedCode && canSave;
// Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up
code = ship.name + (code || '');
// Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time
const _sStr = ship.getStandardString();
const _iStr = ship.getInternalString();
const _hStr = ship.getHardpointsString();
const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`;
const _mStr = ship.getModificationsString();
const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}`;
const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`;
const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`;
const boostMarker = `${ship.canBoost()}`;
const shipSummaryMarker = `${ship.toString()}:${eng}:${fuel}:${cargo}`;
const shipSummaryMarker = `${ship.toString()}${eng}${fuel}${cargo}`;
return (
<div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}>
@@ -386,10 +534,10 @@ export default class OutfittingPage extends Page {
{/* Main tables */}
<ShipSummaryTable ship={ship} marker={shipSummaryMarker} eng={eng} sys={sys} wep={wep} cargo={cargo} fuel={fuel}/>
<StandardSlotSection ship={ship} code={code} onChange={shipUpdated} currentMenu={menu} />
<InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} />
<HardpointsSlotSection ship={ship} code={hStr || ''} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hStr || ''} onChange={shipUpdated} currentMenu={menu} />
<StandardSlotSection ship={ship} code={standardSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<InternalSlotSection ship={ship} code={internalSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<HardpointsSlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
{/* Control of ship and opponent */}
<div className='group quarter'>
@@ -397,28 +545,28 @@ export default class OutfittingPage extends Page {
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('ship control')}</h2>
</div>
<div className='group half'>
<Boost marker={boostMarker} ship={ship} onChange={this._boostUpdated} />
<Boost marker={boostMarker} ship={ship} boost={boost} onChange={this._boostUpdated} />
</div>
</div>
<div className='group quarter'>
<Pips ship={ship} onChange={this._pipsUpdated} />
<Pips sys={sys} eng={eng} wep={wep} onChange={this._pipsUpdated} />
</div>
<div className='group quarter'>
<Fuel ship={ship} onChange={this._fuelUpdated}/>
<Fuel fuelCapacity={ship.fuelCapacity} fuel={fuel} onChange={this._fuelUpdated}/>
</div>
<div className='group quarter'>
{ ship.cargoCapacity > 0 ? <Cargo ship={ship} onChange={this._cargoUpdated}/> : null }
{ ship.cargoCapacity > 0 ? <Cargo cargoCapacity={ship.cargoCapacity} cargo={cargo} onChange={this._cargoUpdated}/> : null }
</div>
<div className='group half'>
<div className='group quarter'>
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('opponent')}</h2>
</div>
<div className='group threequarters'>
<ShipPicker onChange={this._opponentUpdated}/>
<ShipPicker ship={opponent.id} build={opponentBuild} onChange={this._opponentUpdated}/>
</div>
</div>
<div className='group half'>
<EngagementRange ship={ship} onChange={this._engagementRangeUpdated}/>
<EngagementRange ship={ship} engagementRange={engagementRange} onChange={this._engagementRangeUpdated}/>
</div>
{/* Tabbed subpages */}

View File

@@ -808,9 +808,9 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour
/**
* Calculate time to drain WEP capacitor
* @param {object} ship The ship
* @param {number} wep Pips to WEP
* @return The time to drain the WEP capacitor, in seconds
* @param {object} ship The ship
* @param {number} wep Pips to WEP
* @returns {number} The time to drain the WEP capacitor, in seconds
*/
export function timeToDrainWep(ship, wep) {
let totalSEps = 0;
@@ -835,6 +835,12 @@ export function timeToDrainWep(ship, wep) {
/**
* Calculate the time to deplete an amount of shields or armour
* @param {number} amount The amount to be depleted
* @param {number} dps The depletion per second
* @param {number} eps The energy drained per second
* @param {number} capacity The initial energy capacity
* @param {number} recharge The energy recharged per second
* @returns {number} The number of seconds to deplete to 0
*/
export function timeToDeplete(amount, dps, eps, capacity, recharge) {
const drainPerSecond = eps - recharge;