From 91cab5a4f1df2ec95e2d517a5505fd503b66e5da Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 17 Mar 2017 12:07:18 +0000 Subject: [PATCH] Embed battle centre in main pages --- __tests__/fixtures/expected-builds.json | 2 +- src/app/components/BattleCentre.jsx | 144 ------- src/app/components/Boost.jsx | 116 +++++ src/app/components/Defence.jsx | 12 +- src/app/components/EngineProfile.jsx | 72 +--- src/app/components/FSDProfile.jsx | 65 +-- src/app/components/LineChart.jsx | 2 +- src/app/components/Movement.jsx | 1 + src/app/components/Offence.jsx | 498 ++++++++++++++++++++++ src/app/components/OutfittingSubpages.jsx | 156 +++++++ src/app/components/PieChart.jsx | 4 + src/app/components/Pips.jsx | 59 +-- src/app/components/ShipPicker.jsx | 5 +- src/app/pages/OutfittingPage.jsx | 148 +++++-- src/less/app.less | 1 + src/less/boost.less | 14 + src/less/outfit.less | 8 + src/less/pips.less | 13 - src/less/shippicker.less | 5 +- 19 files changed, 976 insertions(+), 349 deletions(-) delete mode 100644 src/app/components/BattleCentre.jsx create mode 100644 src/app/components/Boost.jsx create mode 100644 src/app/components/Offence.jsx create mode 100644 src/app/components/OutfittingSubpages.jsx create mode 100755 src/less/boost.less diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json index 6cfc0f68..2e5498a6 100644 --- a/__tests__/fixtures/expected-builds.json +++ b/__tests__/fixtures/expected-builds.json @@ -36,7 +36,7 @@ "Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=." }, "diamondback_explorer": { - "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI." + "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo." }, "vulture": { "Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA." diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx deleted file mode 100644 index 2ffdfd59..00000000 --- a/src/app/components/BattleCentre.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from 'react'; -import TranslatedComponent from './TranslatedComponent'; -import Ship from '../shipyard/Ship'; -import { Ships } from 'coriolis-data/dist'; -import Slider from './Slider'; -import Pips from './Pips'; -import Fuel from './Fuel'; -import Cargo from './Cargo'; -import Movement from './Movement'; -import EngagementRange from './EngagementRange'; -import ShipPicker from './ShipPicker'; -import Defence from './Defence'; - -/** - * Battle centre allows you to pit your current build against another ship, - * adjust pips and engagement range, and see a wide variety of information - */ -export default class BattleCentre extends TranslatedComponent { - static propTypes = { - ship: React.PropTypes.object.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - - const { ship } = this.props; - - this._cargoUpdated = this._cargoUpdated.bind(this); - this._fuelUpdated = this._fuelUpdated.bind(this); - this._pipsUpdated = this._pipsUpdated.bind(this); - this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); - this._opponentUpdated = this._opponentUpdated.bind(this); - - this.state = { - sys: 2, - eng: 2, - wep: 2, - fuel: ship.fuelCapacity, - cargo: ship.cargoCapacity, - boost: false, - engagementRange: 1500, - opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults) - }; - } - - /** - * Update state based on property and context changes - * @param {Object} nextProps Incoming/Next properties - * @returns {boolean} true if an update is required - */ - componentWillReceiveProps(nextProps) { - // Rather than try to keep track of what changes our children require we force an update and let them work it out - this.forceUpdate(); - return true; - } - - /** - * Triggered when pips have been updated - * @param {number} sys SYS pips - * @param {number} eng ENG pips - * @param {number} wep WEP pips - * @param {boolean} boost true if boosting - */ - _pipsUpdated(sys, eng, wep, boost) { - this.setState({ sys, eng, wep, boost }); - } - - /** - * Triggered when fuel has been updated - * @param {number} fuel the amount of fuel, in T - */ - _fuelUpdated(fuel) { - this.setState({ fuel }); - } - - /** - * Triggered when cargo has been updated - * @param {number} cargo the amount of cargo, in T - */ - _cargoUpdated(cargo) { - this.setState({ cargo }); - } - - /** - * Triggered when engagement range has been updated - * @param {number} engagementRange the engagement range, in m - */ - _engagementRangeUpdated(engagementRange) { - this.setState({ engagementRange }); - } - - /** - * Triggered when target ship has been updated - * @param {object} opponent the opponent's ship - * @param {string} opponentBuild the name of the opponent's build - */ - _opponentUpdated(opponent, opponentBuild) { - this.setState({ opponent, opponentBuild }); - } - - /** - * Render - * @return {React.Component} contents - */ - render() { - const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; - const { formats, translate, units } = language; - const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.state; - const { ship } = this.props; - - // Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time - const pipsMarker = '' + ship.canBoost(); - const movementMarker = '' + ship.topSpeed + ':' + ship.pitch + ':' + ship.roll + ':' + ship.yaw + ':' + ship.canBoost(); - const shieldMarker = '' + ship.shield + ':' + ship.shieldCells + ':' + ship.shieldExplRes + ':' + ship.shieldKinRes + ':' + ship.shieldThermRes + ':' + ship.armour + ship.standard[4].m.getSystemsCapacity() + ':' + ship.standard[4].m.getSystemsRechargeRate() + ':' + opponent.name + ':' + opponentBuild + ':' + engagementRange; - - return ( - -

{translate('battle centre')}

-
-

{translate('ship management')}

- - - { ship.cargoCapacity > 0 ? : null } -

{translate('opponent')}

- - -
-
-

{translate('movement')}

- -
-
-

{translate('defence')}

- -
-
- ); - } -} diff --git a/src/app/components/Boost.jsx b/src/app/components/Boost.jsx new file mode 100644 index 00000000..cc39d28e --- /dev/null +++ b/src/app/components/Boost.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { Pip } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; + +/** + * Boost displays a boost button that toggles bosot + * Requires an onChange() function of the form onChange(boost) which is triggered whenever the boost changes. + */ +export default class Boost extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + const ship = props.ship; + + this._keyDown = this._keyDown.bind(this); + this._toggleBoost = this._toggleBoost.bind(this); + + this.state = { + boost: false + }; + } + + /** + * Add listeners after mounting + */ + componentDidMount() { + document.addEventListener('keydown', this._keyDown); + } + + /** + * Remove listeners before unmounting + */ + componentWillUnmount() { + 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 + */ + _keyDown(e) { + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + switch (e.keyCode) { + case 66: // b == boost + if (this.props.ship.canBoost()) { + e.preventDefault(); + this._toggleBoost(); + } + break; + } + } + } + + /** + * Toggle the boost feature + */ + _toggleBoost() { + let { boost } = this.state; + boost = !boost; + this.setState({ boost }); + this.props.onChange(boost); + } + + /** + * Render boost + * @return {React.Component} contents + */ + render() { + const { formats, translate, units } = this.context.language; + const { ship } = this.props; + const { boost } = this.state; + + // TODO disable if ship cannot boost + return ( + + + + ); + } +} diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 7531a91b..9a32f290 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -182,10 +182,10 @@ export default class Defence extends TranslatedComponent { if (sys === 0) { // No system pips so will never recover shields recover = Math.Inf; - } else { - // Recover remaining shields at the rate of the power distributor's recharge + } else { + // Recover remaining shields at the rate of the power distributor's recharge recover += remainingShieldToRecover / (sysRechargeRate / 0.6); - } + } } // Recharge time is the time taken to go from 50% to 100% @@ -207,10 +207,10 @@ export default class Defence extends TranslatedComponent { if (sys === 0) { // No system pips so will never recharge shields recharge = Math.Inf; - } else { - // Recharge remaining shields at the rate of the power distributor's recharge + } else { + // Recharge remaining shields at the rate of the power distributor's recharge recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6); - } + } } shield = { diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index 0ff10850..fef2a5f5 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -16,7 +16,11 @@ export default class EngineProfile extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +34,7 @@ export default class EngineProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, ship) + calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost) }; } @@ -42,36 +45,23 @@ export default class EngineProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) }); } return true; } /** - * Calculate the maximum speed for this ship across its applicable mass + * Calculate the top speed for this ship given thrusters, mass and pips to ENG * @param {Object} ship The ship + * @param {Object} eng The number of pips to ENG + * @param {Object} boost If boost is enabled * @param {Object} mass The mass at which to calculate the top speed * @return {number} The maximum speed */ - _calcMaxSpeed(ship, mass) { - // Obtain the thrusters for this ship - const thrusters = ship.standard[1].m; - + calcMaxSpeed(ship, eng, boost, mass) { // Obtain the top speed - return Calc.speed(mass, ship.speed, thrusters, ship.engpip)[4]; - } - - /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); + return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost); } /** @@ -81,24 +71,21 @@ export default class EngineProfile extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, eng, fuel, boost } = this.props; // Calculate bounds for our line chart const thrusters = ship.standard[1].m; const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); - let mass = ship.unladenMass + ship.fuelCapacity + cargo; - const minSpeed = Calc.speed(maxMass, ship.speed, thrusters, ship.engpip)[4]; - const maxSpeed = Calc.speed(minMass, ship.speed, thrusters, ship.engpip)[4]; + const mass = ship.unladenMass + fuel + cargo; + const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false); + const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true); // Add a mark at our current mass const mark = Math.min(mass, maxMass); - const cargoPercent = cargo / ship.cargoCapacity; + const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`; - const code = ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); - - // This graph has a precipitous fall-off so we use lots of points to make it look a little smoother + // This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother return (

{translate('engine profile')}

@@ -117,27 +104,6 @@ export default class EngineProfile extends TranslatedComponent { points={1000} code={code} /> - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' }
); } diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index b5138f9c..2db23b1d 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -16,7 +16,9 @@ export default class FSDProfile extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, - code: React.PropTypes.string.isRequired + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired }; /** @@ -30,8 +32,7 @@ export default class FSDProfile extends TranslatedComponent { const ship = this.props.ship; this.state = { - cargo: ship.cargoCapacity, - calcMaxRangeFunc: this._calcMaxRange.bind(this, ship) + calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel) }; } @@ -42,8 +43,8 @@ export default class FSDProfile extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps, nextContext) { - if (nextProps.code != this.props.code) { - this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship) }); + if (nextProps.marker != this.props.marker) { + this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) }); } return true; } @@ -51,38 +52,23 @@ export default class FSDProfile extends TranslatedComponent { /** * Calculate the maximum range for this ship across its applicable mass * @param {Object} ship The ship + * @param {Object} fuel The fuel on the ship * @param {Object} mass The mass at which to calculate the maximum range * @return {number} The maximum range */ - _calcMaxRange(ship, mass) { - // Obtain the FSD for this ship - const fsd = ship.standard[2].m; - + _calcMaxRange(ship, fuel, mass) { // Obtain the maximum range - return Calc.jumpRange(mass, fsd, fsd.getMaxFuelPerJump()); + return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump())); } /** - * Update cargo level - * @param {number} cargoLevel Cargo level 0 - 1 - */ - _cargoChange(cargoLevel) { - let ship = this.props.ship; - let cargo = Math.round(ship.cargoCapacity * cargoLevel); - this.setState({ - cargo - }); - } - - /** - * Render engine profile + * Render FSD profile * @return {React.Component} contents */ render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { ship } = this.props; - const { cargo } = this.state; + const { ship, cargo, fuel } = this.props; // Calculate bounds for our line chart - use thruster info for X @@ -90,15 +76,13 @@ export default class FSDProfile extends TranslatedComponent { const fsd = ship.standard[2].m; const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const maxMass = thrusters.getMaxMass(); - let mass = ship.unladenMass + ship.fuelCapacity + cargo; - const minRange = Calc.jumpRange(maxMass, fsd, ship.fuelCapacity); + const mass = ship.unladenMass + fuel + cargo; + const minRange = 0; const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump()); // Add a mark at our current mass const mark = Math.min(mass, maxMass); - const cargoPercent = cargo / ship.cargoCapacity; - - const code = ship.name + ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); + const code = ship.name + ship.toString() + '.' + fuel; return ( @@ -118,27 +102,6 @@ export default class FSDProfile extends TranslatedComponent { points={200} code={code} /> - {ship.cargoCapacity ? - -

{translate('cargo carried')}: {formats.int(cargo)}{units.T}

- - - - - - -
- -
-
: '' }
); } diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index dbd18b16..38fc1c81 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -116,7 +116,7 @@ export default class LineChart extends TranslatedComponent { _updateDimensions(props, scale) { let { width, xMax, xMin, yMin, yMax } = props; let innerWidth = width - MARGIN.left - MARGIN.right; - let outerHeight = Math.round(width * 0.5 * scale); + let outerHeight = Math.round(width * 0.8 * scale); let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index e03f32c7..6f84a052 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -33,6 +33,7 @@ export default class Movement extends TranslatedComponent { return ( +

{translate('movement profile')}

// Axes diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx new file mode 100644 index 00000000..86307353 --- /dev/null +++ b/src/app/components/Offence.jsx @@ -0,0 +1,498 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; +import * as Calc from '../shipyard/Calculations'; +import { DamageAbsolute, DamageExplosive, DamageKinetic, DamageThermal } from './SvgIcons'; +import PieChart from './PieChart'; +import VerticalBarChart from './VerticalBarChart'; + +/** + * Offence information + */ +export default class Offence extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + engagementrange: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(props.ship, props.opponent, props.sys, props.engagementrange); + this.state = { shield, armour, shielddamage, armourdamage }; + } + + /** + * Update the state if our properties change + * @param {Object} nextProps Incoming/Next properties + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps) { + if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.engagementrange); + this.setState({ shield, armour, shielddamage, armourdamage }); + return true; + } + } + + /** + * Calculate the sustained DPS for a ship at a given range, excluding resistances + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ + _calcSustainedDps(ship, opponent, engagementrange) { + const shieldsdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + const armoursdps = { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0 + }; + + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].m && ship.hardpoints[i].enabled && ship.hardpoints[i].maxClass > 0) { + const m = ship.hardpoints[i].m; + // Initial sustained DPS + let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps(); + // Take fall-off in to account + const falloff = m.getFalloff(); + if (falloff && engagementrange > falloff) { + const dropoffRange = m.getRange() - falloff; + sDps *= 1 - Math.min((engagementrange - falloff) / dropoffRange, 1); + } + // Piercing/hardness modifier (for armour only) + const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness; + // Break out the damage according to type + if (m.getDamageDist().A) { + shieldsdps.absolute += sDps * m.getDamageDist().A; + armoursdps.absolute += sDps * m.getDamageDist().A * armourMultiple; + } + if (m.getDamageDist().E) { + shieldsdps.explosive += sDps * m.getDamageDist().E; + armoursdps.explosive += sDps * m.getDamageDist().E * armourMultiple; + } + if (m.getDamageDist().K) { + shieldsdps.kinetic += sDps * m.getDamageDist().K; + armoursdps.kinetic += sDps * m.getDamageDist().K * armourMultiple; + } + if (m.getDamageDist().T) { + shieldsdps.thermal += sDps * m.getDamageDist().T; + armoursdps.thermal += sDps * m.getDamageDist().T * armourMultiple; + } + } + } + return { shieldsdps, armoursdps }; + } + + /** + * Obtain the recharge rate of the SYS capacitor of a power distributor given pips + * @param {Object} pd The power distributor + * @param {number} sys The number of pips to SYS + * @returns {number} The recharge rate in MJ/s + */ + _calcSysRechargeRate(pd, sys) { + return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1); + } + + /** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The opponent ship + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Shield metrics + */ + _calcMetrics(ship, opponent, sys, engagementrange) { + const sysResistance = this._calcSysResistance(sys); + const maxSysResistance = this._calcSysResistance(4); + + // Obtain the opponent's sustained DPS on us for later damage calculations + const { shieldsdps, armoursdps } = this._calcSustainedDps(opponent, ship, engagementrange); + + let shielddamage = {}; + let shield = {}; + const shieldGeneratorSlot = ship.findInternalByGroup('sg'); + if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { + const shieldGenerator = shieldGeneratorSlot.m; + + // Boosters + let boost = 1; + let boosterExplDmg = 1; + let boosterKinDmg = 1; + let boosterThermDmg = 1; + for (let slot of ship.hardpoints) { + if (slot.enabled && slot.m && slot.m.grp == 'sb') { + boost += slot.m.getShieldBoost(); + boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance()); + boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance()); + boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance()); + } + } + + // Calculate diminishing returns for boosters + boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5); + // Remove base shield generator strength + boost -= 1; + // Apply diminishing returns + boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; + boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2; + boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2; + + const generatorStrength = Calc.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1); + const boostersStrength = generatorStrength * boost; + + // Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover + const shieldToRecover = (generatorStrength + boostersStrength) / 2; + const powerDistributor = ship.standard[4].m; + const sysRechargeRate = this._calcSysRechargeRate(powerDistributor, sys); + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + let capacitorDrain = (shieldGenerator.getBrokenRegenerationRate() * 0.6) - sysRechargeRate; + let capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recover = 16; + if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) { + // We can recover the entire shield from the capacitor store + recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate(); + } else { + // We can recover some of the shield from the capacitor store + recover += capacitorLifetime; + const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); + if (sys === 0) { + // No system pips so will never recover shields + recover = Math.Inf; + } else { + // Recover remaining shields at the rate of the power distributor's recharge + recover += remainingShieldToRecover / (sysRechargeRate / 0.6); + } + } + + // Recharge time is the time taken to go from 50% to 100% + const shieldToRecharge = (generatorStrength + boostersStrength) / 2; + + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + // 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration + capacitorDrain = (shieldGenerator.getRegenerationRate() * 0.6) - sysRechargeRate; + capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + + let recharge = 0; + if (capacitorDrain <= 0 || shieldToRecharge < capacitorLifetime * shieldGenerator.getRegenerationRate()) { + // We can recharge the entire shield from the capacitor store + recharge += shieldToRecharge / shieldGenerator.getRegenerationRate(); + } else { + // We can recharge some of the shield from the capacitor store + recharge += capacitorLifetime; + const remainingShieldToRecharge = shieldToRecharge - capacitorLifetime * shieldGenerator.getRegenerationRate(); + if (sys === 0) { + // No system pips so will never recharge shields + recharge = Math.Inf; + } else { + // Recharge remaining shields at the rate of the power distributor's recharge + recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6); + } + } + + shield = { + generator: generatorStrength, + boosters: boostersStrength, + cells: ship.shieldCells, + total: generatorStrength + boostersStrength + ship.shieldCells, + recover, + recharge, + }; + + // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips. + // We re-cast these as damage percentages + shield.absolute = { + generator: 1, + boosters: 1, + sys: 1 - sysResistance, + total: 1 - sysResistance, + max: 1 - maxSysResistance + }; + + shield.explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance) + }; + + shield.kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance) + }; + + shield.thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance) + }; + + shielddamage.absolutesdps = shieldsdps.absolute *= shield.absolute.total; + shielddamage.explosivesdps = shieldsdps.explosive *= shield.explosive.total; + shielddamage.kineticsdps = shieldsdps.kinetic *= shield.kinetic.total; + shielddamage.thermalsdps = shieldsdps.thermal *= shield.thermal.total; + shielddamage.totalsdps = shielddamage.absolutesdps + shielddamage.explosivesdps + shielddamage.kineticsdps + shielddamage.thermalsdps; + } + + // Armour from bulkheads + const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost()); + let armourReinforcement = 0; + + let moduleArmour = 0; + let moduleProtection = 1; + + let hullExplDmg = 1; + let hullKinDmg = 1; + let hullThermDmg = 1; + + // Armour from HRPs and module armour from MRPs + for (let slot of ship.internal) { + if (slot.m && slot.m.grp == 'hr') { + armourReinforcement += slot.m.getHullReinforcement(); + // Hull boost for HRPs is applied against the ship's base armour + armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000; + + hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance()); + hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance()); + hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance()); + } + if (slot.m && slot.m.grp == 'mrp') { + moduleArmour += slot.m.getIntegrity(); + moduleProtection = moduleProtection * (1 - slot.m.getProtection()); + } + } + moduleProtection = 1 - moduleProtection; + + // Apply diminishing returns + hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2; + hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2; + hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2; + + const armour = { + bulkheads: armourBulkheads, + reinforcement: armourReinforcement, + modulearmour: moduleArmour, + moduleprotection: moduleProtection, + total: armourBulkheads + armourReinforcement + }; + + // Armour resistances have two components: bulkheads and HRPs + // We re-cast these as damage percentages + armour.absolute = { + bulkheads: 1, + reinforcement: 1, + total: 1 + }; + + armour.explosive = { + bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(), + reinforcement: hullExplDmg, + total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg + }; + + armour.kinetic = { + bulkheads: 1 - ship.bulkheads.m.getKineticResistance(), + reinforcement: hullKinDmg, + total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg + }; + + armour.thermal = { + bulkheads: 1 - ship.bulkheads.m.getThermalResistance(), + reinforcement: hullThermDmg, + total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg + }; + + const armourdamage = { + absolutesdps: armoursdps.absolute *= armour.absolute.total, + explosivesdps: armoursdps.explosive *= armour.explosive.total, + kineticsdps: armoursdps.kinetic *= armour.kinetic.total, + thermalsdps: armoursdps.thermal *= armour.thermal.total + }; + armourdamage.totalsdps = armourdamage.absolutesdps + armourdamage.explosivesdps + armourdamage.kineticsdps + armourdamage.thermalsdps; + + return { shield, armour, shielddamage, armourdamage }; + } + + /** + * Calculate the resistance provided by SYS pips + * @param {integer} sys the value of the SYS pips + * @returns {integer} the resistance for the given pips + */ + _calcSysResistance(sys) { + return Math.pow(sys,0.85) * 0.6 / Math.pow(4,0.85); + } + + /** + * Render shields + * @return {React.Component} contents + */ + render() { + const { ship, sys } = this.props; + const { language, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { shield, armour, shielddamage, armourdamage } = this.state; + + const shieldSourcesData = []; + const effectiveShieldData = []; + const shieldDamageTakenData = []; + const shieldTooltipDetails = []; + const shieldAbsoluteTooltipDetails = []; + const shieldExplosiveTooltipDetails = []; + const shieldKineticTooltipDetails = []; + const shieldThermalTooltipDetails = []; + let maxEffectiveShield = 0; + if (shield.total) { + if (Math.round(shield.generator) > 0) shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') }); + if (Math.round(shield.boosters) > 0) shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); + if (Math.round(shield.cells) > 0) shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') }); + + if (Math.round(shield.generator) > 0) shieldTooltipDetails.push(
{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
); + if (Math.round(shield.boosters) > 0) shieldTooltipDetails.push(
{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
); + if (Math.round(shield.cells) > 0) shieldTooltipDetails.push(
{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
); + + shieldAbsoluteTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}
); + shieldAbsoluteTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}
); + shieldAbsoluteTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}
); + + shieldExplosiveTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}
); + shieldExplosiveTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}
); + shieldExplosiveTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}
); + + shieldKineticTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}
); + shieldKineticTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}
); + shieldKineticTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}
); + + shieldThermalTooltipDetails.push(
{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}
); + shieldThermalTooltipDetails.push(
{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}
); + shieldThermalTooltipDetails.push(
{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}
); + + const effectiveAbsoluteShield = shield.total / shield.absolute.total; + effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') }); + const effectiveExplosiveShield = shield.total / shield.explosive.total; + effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') }); + const effectiveKineticShield = shield.total / shield.kinetic.total; + effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') }); + const effectiveThermalShield = shield.total / shield.thermal.total; + effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') }); + + shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute') }); + shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive') }); + shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic') }); + shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal') }); + + maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max); + } + + const armourSourcesData = []; + if (Math.round(armour.bulkheads) > 0) armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); + if (Math.round(armour.reinforcement) > 0) armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); + + const armourTooltipDetails = []; + if (armour.bulkheads > 0) armourTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
); + if (armour.reinforcement > 0) armourTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
); + + const armourAbsoluteTooltipDetails = []; + armourAbsoluteTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}
); + armourAbsoluteTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}
); + + const armourExplosiveTooltipDetails = []; + armourExplosiveTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}
); + armourExplosiveTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}
); + + const armourKineticTooltipDetails = []; + armourKineticTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}
); + armourKineticTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}
); + + const armourThermalTooltipDetails = []; + armourThermalTooltipDetails.push(
{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}
); + armourThermalTooltipDetails.push(
{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}
); + + const effectiveArmourData = []; + const effectiveAbsoluteArmour = armour.total / armour.absolute.total; + effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute') }); + const effectiveExplosiveArmour = armour.total / armour.explosive.total; + effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive') }); + const effectiveKineticArmour = armour.total / armour.kinetic.total; + effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic') }); + const effectiveThermalArmour = armour.total / armour.thermal.total; + effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal') }); + + const armourDamageTakenData = []; + armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute') }); + armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive') }); + armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic') }); + armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal') }); + + return ( + + {shield.total ? +
+

{translate('shield metrics')}

+
+

{shieldTooltipDetails}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}
{formats.int(shield.total)}{units.MJ} +

{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}
{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(shield.total / shielddamage.totalsdps)}

+

{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}

+

{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}

+ +
+

{translate('shield sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective shield')}(MJ)

+ +
+
: null } + +
+

{translate('armour metrics')}

+

{armourTooltipDetails}

)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}
{formats.int(armour.total)} +

{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}
{armourdamage.totalsdps == 0 ? translate('infinity') : formats.time(armour.total / armourdamage.totalsdps)}

+

{translate('raw module armour')}
{formats.int(armour.modulearmour)}

+

{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}
{formats.pct1(armour.moduleprotection / 2)}

+

{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}
{formats.pct1(armour.moduleprotection)}

+
+ +
+

{translate('armour sources')}

+ +
+
+

{translate('damage taken')}(%)

+ +
+
+

{translate('effective armour')}

+ +
+
); + } +} diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx new file mode 100644 index 00000000..807bf4f7 --- /dev/null +++ b/src/app/components/OutfittingSubpages.jsx @@ -0,0 +1,156 @@ +import React from 'react'; +import cn from 'classnames'; +import { Ships } from 'coriolis-data/dist'; +import Ship from '../shipyard/Ship'; +import { Insurance } from '../shipyard/Constants'; +import { slotName, slotComparator } from '../utils/SlotFunctions'; +import TranslatedComponent from './TranslatedComponent'; +import PowerManagement from './PowerManagement'; +import CostSection from './CostSection'; +import EngineProfile from './EngineProfile'; +import FSDProfile from './FSDProfile'; +import Movement from './Movement'; +import Defence from './Defence'; + +/** + * Outfitting subpages + */ +export default class OutfittingSubpages extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired, + onChange: React.PropTypes.func.isRequired, + chartWidth: React.PropTypes.number.isRequired, + buildName: React.PropTypes.string, + sys: React.PropTypes.number.isRequired, + eng: React.PropTypes.number.isRequired, + wep: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + boost: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + opponent: React.PropTypes.object.isRequired, + opponentBuild: React.PropTypes.string + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._powerTab = this._powerTab.bind(this); + + this.state = { + tab: 'power' + }; + } + + /** + * Show selected tab + * @param {string} tab Tab name + */ + _showTab(tab) { + this.setState({ tab }); + } + + /** + * Render the power tab + * @return {React.Component} Tab contents + */ + _powerTab() { + let { ship, buildName, code, onChange } = this.props; + + return
+ + +
; + } + + /** + * Render the profiles tab + * @return {React.Component} Tab contents + */ + _profilesTab() { + const { ship, code, chartWidth, cargo, fuel, eng, boost } = this.props; + let realBoost = boost && ship.canBoost(); + + const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`; + const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`; + const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost()}`; + + return
+
+ +
+ +
+ +
+ +
+ +
+
; + } + + /** + * Render the offence tab + * @return {React.Component} Tab contents + */ + _offenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + + return
+

Offence goes here

+
; + } + + /** + * Render the defence tab + * @return {React.Component} Tab contents + */ + _defenceTab() { + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + + const marker = `${ship.shield}:${ship.shieldCells}:${ship.shieldExplRes}:${ship.shieldKinRes}:${ship.shieldThermRes}:${ship.armour}:${ship.standard[4].m.getSystemsCapacity()}:${ship.standard[4].m.getSystemsRechargeRate()}:${opponent.name}:${opponentBuild}:${engagementRange}`; + + return
+ +
; + } + + /** + * Render the section + * @return {React.Component} Contents + */ + render() { + const tab = this.state.tab || 'power'; + const translate = this.context.language.translate; + let tabSection; + + switch (tab) { + case 'power': tabSection = this._powerTab(); break; + case 'profiles': tabSection = this._profilesTab(); break; + case 'offence': tabSection = this._offenceTab(); break; + case 'defence': tabSection = this._defenceTab(); break; + } + + return ( +
+ + + + + + + + + +
{translate('power and costs')}{translate('profiles')}{translate('offence')}{translate('defence')}
+ {tabSection} +
+ ); + } +} diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index 0af72456..0bcf98e5 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -10,6 +10,10 @@ const LABEL_COLOUR = '#FFFFFF'; */ export default class PieChart extends Component { + static propTypes = { + data : React.PropTypes.array.isRequired + }; + /** * Constructor * @param {Object} props React Component properties diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index ec8a0ff6..7c9f86ec 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -11,11 +11,10 @@ import Module from '../shipyard/Module'; /** * Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area. - * Requires an onChange() function of the form onChange(sys, eng, wep, boost) which is triggered whenever the pips change. + * Requires an onChange() function of the form onChange(sys, eng, wep) which is triggered whenever the pips change. */ export default class Pips extends TranslatedComponent { static propTypes = { - marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; @@ -31,14 +30,12 @@ export default class Pips extends TranslatedComponent { const pd = ship.standard[4].m; this._keyDown = this._keyDown.bind(this); - this._toggleBoost = this._toggleBoost.bind(this); let pipsSvg = this._renderPips(2, 2, 2); this.state = { sys: 2, eng: 2, wep: 2, - boost: false, sysCap: pd.getSystemsCapacity(), engCap: pd.getEnginesCapacity(), wepCap: pd.getWeaponsCapacity(), @@ -69,7 +66,7 @@ export default class Pips extends TranslatedComponent { * @returns {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps) { - const { sysCap, engCap, wepCap, sysRate, engRate, wepRate, boost } = this.state; + const { sysCap, engCap, wepCap, sysRate, engRate, wepRate } = this.state; const nextShip = nextProps.ship; const pd = nextShip.standard[4].m; @@ -79,22 +76,19 @@ export default class Pips extends TranslatedComponent { const nextSysRate = pd.getSystemsRechargeRate(); const nextEngRate = pd.getEnginesRechargeRate(); const nextWepRate = pd.getWeaponsRechargeRate(); - const nextBoost = nextShip.canBoost() ? boost : false; if (nextSysCap != sysCap || nextEngCap != engCap || nextWepCap != wepCap || nextSysRate != sysRate || nextEngRate != engRate || - nextWepRate != wepRate || - nextBoost != boost) { + nextWepRate != wepRate) { this.setState({ sysCap: nextSysCap, engCap: nextEngCap, wepCap: nextWepCap, sysRate: nextSysRate, engRate: nextEngRate, - wepRate: nextWepRate, - boost: nextBoost + wepRate: nextWepRate }); } @@ -108,12 +102,6 @@ export default class Pips extends TranslatedComponent { _keyDown(e) { if (e.ctrlKey || e.metaKey) { // CTRL/CMD switch (e.keyCode) { - case 66: // b == boost - if (this.props.ship.canBoost()) { - e.preventDefault(); - this._toggleBoost(); - } - break; case 37: // Left arrow == increase SYS e.preventDefault(); this._incSys(); @@ -154,11 +142,11 @@ export default class Pips extends TranslatedComponent { * Reset the capacitor */ _reset() { - let { sys, eng, wep, boost } = this.state; + let { sys, eng, wep } = this.state; 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, boost); + this.props.onChange(sys, eng, wep); } } @@ -166,7 +154,7 @@ export default class Pips extends TranslatedComponent { * Increment the SYS capacitor */ _incSys() { - let { sys, eng, wep, boost } = this.state; + let { sys, eng, wep } = this.state; const required = Math.min(1, 4 - sys); if (required > 0) { @@ -194,7 +182,7 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep, boost); + this.props.onChange(sys, eng, wep); } } @@ -202,7 +190,7 @@ export default class Pips extends TranslatedComponent { * Increment the ENG capacitor */ _incEng() { - let { sys, eng, wep, boost } = this.state; + let { sys, eng, wep } = this.state; const required = Math.min(1, 4 - eng); if (required > 0) { @@ -230,7 +218,7 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep, boost); + this.props.onChange(sys, eng, wep); } } @@ -238,7 +226,7 @@ export default class Pips extends TranslatedComponent { * Increment the WEP capacitor */ _incWep() { - let { sys, eng, wep, boost } = this.state; + let { sys, eng, wep } = this.state; const required = Math.min(1, 4 - wep); if (required > 0) { @@ -266,20 +254,10 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep, boost); + this.props.onChange(sys, eng, wep); } } - /** - * Toggle the boost feature - */ - _toggleBoost() { - let { boost, sys, eng, wep } = this.state; - boost = !boost; - this.setState({ boost }); - this.props.onChange(sys, eng, wep, boost); - } - /** * Set up the rendering for pips * @param {int} sys the SYS pips @@ -336,7 +314,7 @@ export default class Pips extends TranslatedComponent { render() { const { formats, translate, units } = this.context.language; const { ship } = this.props; - const { boost, sys, eng, wep, sysCap, engCap, wepCap, sysRate, engRate, wepRate, pipsSvg } = this.state; + const { sys, eng, wep, sysCap, engCap, wepCap, sysRate, engRate, wepRate, pipsSvg } = this.state; const onSysClicked = this.onClick.bind(this, 'SYS'); const onEngClicked = this.onClick.bind(this, 'ENG'); @@ -344,16 +322,9 @@ export default class Pips extends TranslatedComponent { const onRstClicked = this.onClick.bind(this, 'RST'); return ( -
+ - { ship.canBoost() ? - - - - - - : null } @@ -374,7 +345,7 @@ export default class Pips extends TranslatedComponent {
   
   
-
+
); } } diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index 5b0529a5..cebfa173 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -128,12 +128,13 @@ export default class ShipPicker extends TranslatedComponent { const { formats, translate, units } = language; const { menuOpen, ship, build } = this.state; - const shipString = ship.name + ': ' + (build ? build : 'stock'); + const shipString = ship.name + ': ' + (build ? build : translate('stock')); return (
e.stopPropagation() }>
- {shipString} + + {shipString}
{ menuOpen ?
e.stopPropagation() }> diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 35c43532..7ad4b5d4 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -17,12 +17,13 @@ import UtilitySlotSection from '../components/UtilitySlotSection'; import OffenceSummary from '../components/OffenceSummary'; import DefenceSummary from '../components/DefenceSummary'; import MovementSummary from '../components/MovementSummary'; -import EngineProfile from '../components/EngineProfile'; -import FSDProfile from '../components/FSDProfile'; -import JumpRange from '../components/JumpRange'; -import BattleCentre from '../components/BattleCentre'; -import PowerManagement from '../components/PowerManagement'; -import CostSection from '../components/CostSection'; +import Pips from '../components/Pips'; +import Boost from '../components/Boost'; +import Fuel from '../components/Fuel'; +import Cargo from '../components/Cargo'; +import ShipPicker from '../components/ShipPicker'; +import EngagementRange from '../components/EngagementRange'; +import OutfittingSubpages from '../components/OutfittingSubpages'; import ModalExport from '../components/ModalExport'; import ModalPermalink from '../components/ModalPermalink'; @@ -51,6 +52,12 @@ export default class OutfittingPage extends Page { this.state = this._initState(context); this._keyDown = this._keyDown.bind(this); this._exportBuild = this._exportBuild.bind(this); + this._pipsUpdated = this._pipsUpdated.bind(this); + this._boostUpdated = this._boostUpdated.bind(this); + this._cargoUpdated = this._cargoUpdated.bind(this); + this._fuelUpdated = this._fuelUpdated.bind(this); + this._opponentUpdated = this._opponentUpdated.bind(this); + this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); } /** @@ -90,7 +97,15 @@ export default class OutfittingPage extends Page { shipId, ship, code, - savedCode + savedCode, + sys: 2, + eng: 2, + wep: 2, + fuel: ship.fuelCapacity, + cargo: 0, + boost: false, + engagementRange: 1500, + opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults) }; } @@ -112,6 +127,57 @@ export default class OutfittingPage extends Page { this.setState(stateChanges); } + /** + * Triggered when pips have been updated + * @param {number} sys SYS pips + * @param {number} eng ENG pips + * @param {number} wep WEP pips + */ + _pipsUpdated(sys, eng, wep) { + this.setState({ sys, eng, wep }); + } + + /** + * Triggered when boost has been updated + * @param {boolean} boost true if boosting + */ + _boostUpdated(boost) { + this.setState({ boost }); + } + + /** + * Triggered when fuel has been updated + * @param {number} fuel the amount of fuel, in T + */ + _fuelUpdated(fuel) { + this.setState({ fuel }); + } + + /** + * Triggered when cargo has been updated + * @param {number} cargo the amount of cargo, in T + */ + _cargoUpdated(cargo) { + this.setState({ cargo }); + } + + /** + * Triggered when engagement range has been updated + * @param {number} engagementRange the engagement range, in m + */ + _engagementRangeUpdated(engagementRange) { + this.setState({ engagementRange }); + } + + /** + * Triggered when target ship has been updated + * @param {object} opponent the opponent's ship + * @param {string} opponentBuild the name of the opponent's build + */ + _opponentUpdated(opponent, opponentBuild) { + this.setState({ opponent, opponentBuild }); + } + /** * Save the current build */ @@ -294,7 +360,7 @@ export default class OutfittingPage extends Page { let state = this.state, { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context, { translate, units, formats } = language, - { ship, code, savedCode, buildName, newBuildName, halfChartWidth, thirdChartWidth } = state, + { ship, code, savedCode, buildName, newBuildName, halfChartWidth, thirdChartWidth, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = state, hide = tooltip.bind(null, null), menu = this.props.currentMenu, shipUpdated = this._shipUpdated, @@ -307,6 +373,9 @@ export default class OutfittingPage extends Page { // 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 boostMarker = `${ship.canBoost()}`; + return (
@@ -340,40 +409,59 @@ export default class OutfittingPage extends Page {
+ {/* Main tables */} -
- + {/* Control of ship and opponent */} +
+
+

{translate('ship control')}

+
+
+ +
-
- +
+
-
- +
+
- - - - -
- +
+ { ship.cargoCapacity > 0 ? : null }
- -
- +
+
+

{translate('opponent')}

+
+
+ +
- -
- -
- -
- +
+
+ {/* Tabbed subpages */} +
); diff --git a/src/less/app.less b/src/less/app.less index 3dc03d2d..f1234db6 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -20,6 +20,7 @@ @import 'sortable'; @import 'loader'; @import 'pips'; +@import 'boost'; @import 'movement'; @import 'shippicker'; @import 'defence'; diff --git a/src/less/boost.less b/src/less/boost.less new file mode 100755 index 00000000..75db11be --- /dev/null +++ b/src/less/boost.less @@ -0,0 +1,14 @@ +#boost { + button { + font-size: 1.2em; + background: @primary-bg; + color: @primary; + border: 1px solid @primary; + &.selected { + // Shown when button is selected + background: @primary; + color: @primary-bg; + } + } +} + diff --git a/src/less/outfit.less b/src/less/outfit.less index bccd47f8..092fd499 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -220,6 +220,14 @@ }); } + &.threequarters { + width: 75%; + + .smallTablet({ + width: 100% !important; + }); + } + &.full { width: 100%; diff --git a/src/less/pips.less b/src/less/pips.less index 1db973ae..2d3dccbd 100755 --- a/src/less/pips.less +++ b/src/less/pips.less @@ -12,19 +12,6 @@ cursor: pointer; } - button { - font-size: 1.2em; - background: @primary-bg; - color: @primary; - border: 1px solid @primary; - margin: 20px; - &.selected { - // Shown when button is selected - background: @primary; - color: @primary-bg; - } - } - // A full pip .full { stroke: @primary; diff --git a/src/less/shippicker.less b/src/less/shippicker.less index 11726fff..8c7658c1 100755 --- a/src/less/shippicker.less +++ b/src/less/shippicker.less @@ -1,12 +1,11 @@ .shippicker { background-color: @bgBlack; margin: 0; - padding: 0 0 0 1em; height: 3em; - line-height: 3em; font-family: @fTitle; vertical-align: middle; position: relative; + display: block; .user-select-none(); @@ -34,7 +33,6 @@ cursor: pointer; color: @warning; text-transform: uppercase; - // Less than 600px screen width: hide text &.disabled { color: @warning-disabled; @@ -47,7 +45,6 @@ .menu-item-label { margin-left: 1em; - display: inline-block; .smallTablet({ display: none;