From 3a55c8cc0ab1530c38ddee42c7efea956612c0fe Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 10 Mar 2017 17:17:37 +0000 Subject: [PATCH 01/53] Building components for battle centre --- src/app/components/BattleCentre.jsx | 98 +++++++ src/app/components/Cargo.jsx | 102 +++++++ src/app/components/EngagementRange.jsx | 123 +++++++++ src/app/components/Fuel.jsx | 96 +++++++ src/app/components/Pips.jsx | 358 +++++++++++++++++++++++++ src/app/components/SvgIcons.jsx | 18 ++ src/app/i18n/Language.jsx | 22 +- src/app/pages/OutfittingPage.jsx | 10 +- src/less/app.less | 1 + src/less/outfit.less | 8 + src/less/pips.less | 29 ++ 11 files changed, 846 insertions(+), 19 deletions(-) create mode 100644 src/app/components/BattleCentre.jsx create mode 100644 src/app/components/Cargo.jsx create mode 100644 src/app/components/EngagementRange.jsx create mode 100644 src/app/components/Fuel.jsx create mode 100644 src/app/components/Pips.jsx create mode 100755 src/less/pips.less diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx new file mode 100644 index 00000000..fc6297ea --- /dev/null +++ b/src/app/components/BattleCentre.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; +import Pips from '../components/Pips'; +import Fuel from '../components/Fuel'; +import Cargo from '../components/Cargo'; +import EngagementRange from '../components/EngagementRange'; + +/** + * 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 + }; + + static DEFAULT_OPPONENT = { ship: Ships['anaconda'] }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + const { ship } = this.props; + const opponent = BattleCentre.DEFAULT_OPPONENT; + + this.state = { }; + } + + 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 + */ + _pipsUpdated(sys, eng, wep) { + console.log('Pips are now ' + sys + '/' + eng + '/' + wep); + } + + /** + * Triggered when fuel has been updated + */ + _fuelUpdated(fuel) { + console.log('Fuel is now ' + fuel); + } + + /** + * Triggered when cargo has been updated + */ + _cargoUpdated(cargo) { + console.log('Cargo is now ' + cargo); + } + + /** + * Triggered when engagement range has been updated + */ + _engagementRangeUpdated(engagementRange) { + console.log('Engagement range is now ' + engagementRange); + } + + /** + * Render + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { against, expanded, maxRange, range, totals } = this.state; + const { ship } = this.props; + const shipUpdated = this._shipUpdated; + const pipsUpdated = this._pipsUpdated; + const fuelUpdated = this._fuelUpdated; + const cargoUpdated = this._cargoUpdated; + const engagementRangeUpdated = this._engagementRangeUpdated; + + return ( + +

{translate('battle centre')}

+
+ +
+
+ + + +
+
+ ); + } +} diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx new file mode 100644 index 00000000..1e7ddbaf --- /dev/null +++ b/src/app/components/Cargo.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Cargo slider + * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change + */ +export default class Cargo extends TranslatedComponent { + static PropTypes = { + 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 = this.props.ship; + + this.state = { + cargoCapacity: ship.cargoCapacity, + cargoLevel: 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 { 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; + } + + /** + * Update cargo level + * @param {number} cargoLevel percentage level from 0 to 1 + */ + _cargoChange(cargoLevel) { + const { cargoCapacity } = this.state; + // 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)); + } + } + + /** + * Render cargo slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { cargoLevel, cargoCapacity } = this.state; + + return ( + +

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

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx new file mode 100644 index 00000000..adfee2cd --- /dev/null +++ b/src/app/components/EngagementRange.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +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 { + static PropTypes = { + 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 = this.props.ship; + + const maxRange = this._calcMaxRange(ship); + + this.state = { + maxRange: maxRange, + rangeLevel: 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 { 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 + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Update range + * @param {number} range percentage level from 0 to 1 + */ + _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)); + } + } + + /** + * Render range slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { rangeLevel, maxRange } = this.state; + + return ( + +

{translate('engagement range')}: {formats.int(rangeLevel * maxRange)}{translate('m')}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx new file mode 100644 index 00000000..eb5d98c6 --- /dev/null +++ b/src/app/components/Fuel.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Fuel slider + * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change + */ +export default class Fuel extends TranslatedComponent { + static PropTypes = { + 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 = 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; + } + + /** + * Update fuel level + * @param {number} fuelLevel percentage level from 0 to 1 + */ + _fuelChange(fuelLevel) { + this.setState({ fuelLevel }); + this.props.onChange(fuelLevel * this.state.fuelCapacity); + } + + /** + * Render fuel slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { fuelLevel, fuelCapacity } = this.state; + + return ( + +

{translate('fuel carried')}: {formats.f2(fuelLevel * fuelCapacity)}{units.T}

+ + + + + + +
+ +
+
+ ); + } +} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx new file mode 100644 index 00000000..b6ecd59a --- /dev/null +++ b/src/app/components/Pips.jsx @@ -0,0 +1,358 @@ +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'; + +/** + * 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) which is triggered whenever the pips change. + */ +export default class Pips extends TranslatedComponent { + static PropTypes = { + 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; + const pd = ship.standard[4].m; + + 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 + }; + } + + /** + * 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 { sysCap, engCap, wepCap, sysRate, engRate, wepRate } = this.state; + const ship = nextProps.ship; + const pd = ship.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 + */ + _keyDown(e) { + switch (e.keyCode) { + case 37: // Left arrow == increase SYS + e.preventDefault(); + this._incSys(); + break; + case 38: // Up arrow == increase ENG + e.preventDefault(); + this._incEng(); + break; + case 39: // Right arrow == increase WEP + e.preventDefault(); + this._incWep(); + break; + case 40: // Down arrow == reset + e.preventDefault(); + this._reset(); + break; + } + } + + /** + * Reset the capacitor + */ + _reset() { + 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); + } + } + + /** + * Increment the SYS capacitor + */ + _incSys() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - sys); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (eng > wep) { + eng -= 0.5; + sys += 0.5; + } else { + wep -= 0.5; + sys += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (eng == 0) { + wep -= 1; + sys += 1; + } else if (wep == 0) { + eng -= 1; + sys += 1; + } else { + eng -= 0.5; + wep -= 0.5; + sys += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the ENG capacitor + */ + _incEng() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - eng); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > wep) { + sys -= 0.5; + eng += 0.5; + } else { + wep -= 0.5; + eng += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + wep -= 1; + eng += 1; + } else if (wep == 0) { + sys -= 1; + eng += 1; + } else { + sys -= 0.5; + wep -= 0.5; + eng += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the WEP capacitor + */ + _incWep() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - wep); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > eng) { + sys -= 0.5; + wep += 0.5; + } else { + eng -= 0.5; + wep += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + eng -= 1; + wep += 1; + } else if (eng == 0) { + sys -= 1; + wep += 1; + } else { + sys -= 0.5; + eng -= 0.5; + wep += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Handle a click + */ + onClick(which) { + if (which == 'SYS') { + this._incSys(); + } else if (which == 'ENG') { + this._incEng(); + } else if (which == 'WEP') { + this._incWep(); + } else if (which == 'RST') { + this._reset(); + } + } + + /** + * Set up the rendering for pips + * @param {int} sys the SYS pips + * @param {int} eng the ENG pips + * @param {int} wep the WEP pips + * @returns {Object} Object containing the rendering for the pips + */ + _renderPips(sys, eng, wep) { + const pipsSvg = {}; + + // SYS + pipsSvg['SYS'] = []; + for (let i = 0; i < Math.floor(sys); i++) { + pipsSvg['SYS'].push(); + } + if (sys > Math.floor(sys)) { + pipsSvg['SYS'].push(); + } + for (let i = Math.floor(sys + 0.5); i < 4; i++) { + pipsSvg['SYS'].push(); + } + + // ENG + pipsSvg['ENG'] = []; + for (let i = 0; i < Math.floor(eng); i++) { + pipsSvg['ENG'].push(); + } + if (eng > Math.floor(eng)) { + pipsSvg['ENG'].push(); + } + for (let i = Math.floor(eng + 0.5); i < 4; i++) { + pipsSvg['ENG'].push(); + } + + // WEP + pipsSvg['WEP'] = []; + for (let i = 0; i < Math.floor(wep); i++) { + pipsSvg['WEP'].push(); + } + if (wep > Math.floor(wep)) { + pipsSvg['WEP'].push(); + } + for (let i = Math.floor(wep + 0.5); i < 4; i++) { + pipsSvg['WEP'].push(); + } + + return pipsSvg; + } + + /** + * Render pips + * @return {React.Component} contents + */ + 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 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'); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  {pipsSvg['ENG']} 
 {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
 {translate('SYS')}{translate('RST')}{translate('WEP')}
{translate('capacity')} ({units.MJ}){formats.f1(sysCap)}{formats.f1(engCap)}{formats.f1(wepCap)}
{translate('recharge')} ({units.MW}){formats.f1(sysRate * (sys / 4))}{formats.f1(engRate * (eng / 4))}{formats.f1(wepRate * (wep / 4))}
+ ); + } +} diff --git a/src/app/components/SvgIcons.jsx b/src/app/components/SvgIcons.jsx index af119679..4593f173 100644 --- a/src/app/components/SvgIcons.jsx +++ b/src/app/components/SvgIcons.jsx @@ -708,6 +708,24 @@ export class Switch extends SvgIcon { } } +/** + * Pip + */ +export class Pip extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return ; + } +} + /** * In-game Coriolis Station logo */ diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index f22c0d42..e8d81da0 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -58,21 +58,21 @@ export function getLanguage(langCode) { }, translate, units: { - CR: {translate('CR')}, // Credits - kg: {translate('kg')}, // Kilograms - kgs: {translate('kg/s')}, // Kilograms per second - km: {translate('km')}, // Kilometers - Ls: {translate('Ls')}, // Light Seconds - LY: {translate('LY')}, // Light Years - MJ: {translate('MJ')}, // Mega Joules - 'm/s': {translate('m/s')}, // Meters per second - '°/s': {translate('°/s')}, // Degrees per second - MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) + CR: {translate('CR')}, // Credits + kg: {translate('kg')}, // Kilograms + kgs: {translate('kg/s')}, // Kilograms per second + km: {translate('km')}, // Kilometers + Ls: {translate('Ls')}, // Light Seconds + LY: {translate('LY')}, // Light Years + MJ: {translate('MJ')}, // Mega Joules + 'm/s': {translate('m/s')}, // Meters per second + '°/s': {translate('°/s')}, // Degrees per second + MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) mps: {translate('m/s')}, // Metres per second ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute s: {translate('secs')}, // Seconds - T: {translate('T')}, // Metric Tons + T: {translate('T')}, // Metric Tons } }; } diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 0e50fc63..fb0ffbd3 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -20,13 +20,11 @@ import MovementSummary from '../components/MovementSummary'; import EngineProfile from '../components/EngineProfile'; import FSDProfile from '../components/FSDProfile'; import JumpRange from '../components/JumpRange'; -import DamageDealt from '../components/DamageDealt'; -import DamageReceived from '../components/DamageReceived'; +import BattleCentre from '../components/BattleCentre'; import PowerManagement from '../components/PowerManagement'; import CostSection from '../components/CostSection'; import ModalExport from '../components/ModalExport'; import ModalPermalink from '../components/ModalPermalink'; -import Slider from '../components/Slider'; /** * Document Title Generator @@ -373,11 +371,7 @@ export default class OutfittingPage extends Page {
- -
- -
- +
diff --git a/src/less/app.less b/src/less/app.less index ccff47d7..25d4de4a 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -19,6 +19,7 @@ @import 'shipselector'; @import 'sortable'; @import 'loader'; +@import 'pips'; html, body { height: 100%; diff --git a/src/less/outfit.less b/src/less/outfit.less index 8d5e7f0e..7b792542 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -198,6 +198,14 @@ }); } + &.twothirds { + width: 67%; + + .smallTablet({ + width: 100% !important; + }); + } + .smallScreen({ .axis.x { g.tick:nth-child(2n + 1) text { diff --git a/src/less/pips.less b/src/less/pips.less new file mode 100755 index 00000000..d1b09f43 --- /dev/null +++ b/src/less/pips.less @@ -0,0 +1,29 @@ +// The pips table - keep the background black +.pipstable { + background-color: @bgBlack; + color: @primary; +} + +// A clickable entity in the pips table +.pipsclickable { + cursor: pointer; +} + +// A full pip +.fullpip { + stroke: @primary; + fill: @primary; +} + +// A half pip +.halfpip { + stroke: @primary-disabled; + fill: @primary-disabled; +} + +// An empty pip +.emptypip { + stroke: @primary-bg; + fill: @primary-bg; +} + From 02db800c7b781155640e679aaec583245c2bac84 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 13:28:24 +0000 Subject: [PATCH 02/53] Link in components --- src/app/components/BattleCentre.jsx | 45 ++++++++++----- src/app/components/Cargo.jsx | 12 ++-- src/app/components/Movement.jsx | 85 +++++++++++++++++++++++++++++ src/app/shipyard/Calculations.js | 79 +++++++++++++++++++++++++++ src/app/shipyard/Ship.js | 48 ++++++++++++++++ src/less/app.less | 1 + src/less/movement.less | 26 +++++++++ 7 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 src/app/components/Movement.jsx create mode 100644 src/less/movement.less diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index fc6297ea..c5187ff5 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -5,6 +5,7 @@ import Slider from '../components/Slider'; import Pips from '../components/Pips'; import Fuel from '../components/Fuel'; import Cargo from '../components/Cargo'; +import Movement from '../components/Movement'; import EngagementRange from '../components/EngagementRange'; /** @@ -29,7 +30,23 @@ export default class BattleCentre extends TranslatedComponent { const { ship } = this.props; const opponent = BattleCentre.DEFAULT_OPPONENT; - this.state = { }; + 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.state = { + // Pips + sys: 2, + eng: 2, + wep: 2, + // Fuel + fuel: ship.fuelCapacity, + // Cargo + cargo: ship.cargoCapacity, + // Engagement range + engagementRange: 1500, + }; } componentWillReceiveProps(nextProps) { @@ -42,28 +59,28 @@ export default class BattleCentre extends TranslatedComponent { * Triggered when pips have been updated */ _pipsUpdated(sys, eng, wep) { - console.log('Pips are now ' + sys + '/' + eng + '/' + wep); + this.setState({ sys, eng, wep }); } /** * Triggered when fuel has been updated */ _fuelUpdated(fuel) { - console.log('Fuel is now ' + fuel); + this.setState({ fuel }); } /** * Triggered when cargo has been updated */ _cargoUpdated(cargo) { - console.log('Cargo is now ' + cargo); + this.setState({ cargo }); } /** * Triggered when engagement range has been updated */ _engagementRangeUpdated(engagementRange) { - console.log('Engagement range is now ' + engagementRange); + this.setState({ engagementRange }); } /** @@ -73,24 +90,22 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { against, expanded, maxRange, range, totals } = this.state; + const { sys, eng, wep, cargo, fuel, engagementRange, totals } = this.state; const { ship } = this.props; - const shipUpdated = this._shipUpdated; - const pipsUpdated = this._pipsUpdated; - const fuelUpdated = this._fuelUpdated; - const cargoUpdated = this._cargoUpdated; - const engagementRangeUpdated = this._engagementRangeUpdated; return (

{translate('battle centre')}

- +
- - - + + + +
+
+
); diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index 1e7ddbaf..f03ae01e 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -58,12 +58,14 @@ export default class Cargo extends TranslatedComponent { */ _cargoChange(cargoLevel) { const { cargoCapacity } = this.state; - // We round the cargo level to a suitable value given the capacity - cargoLevel = Math.round(cargoLevel * cargoCapacity) / cargoCapacity; + 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)); + if (cargoLevel != this.state.cargoLevel) { + this.setState({ cargoLevel }); + this.props.onChange(Math.round(cargoLevel * cargoCapacity)); + } } } diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx new file mode 100644 index 00000000..3cf1dc8e --- /dev/null +++ b/src/app/components/Movement.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; + +/** + * Movement + */ +export default class Movement extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired, + eng: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._toggleBoost = this._toggleBoost.bind(this); + + this.state = { boost: false }; + } + + /** + * Toggle the boost feature + */ + _toggleBoost() { + let { boost } = this.state; + boost = !boost; + this.setState({ boost }); + } + + /** + * Render movement + * @return {React.Component} contents + */ + render() { + const { ship, eng, cargo, fuel } = this.props; + const { language } = this.context; + const { formats, translate, units } = language; + const { boost } = this.state; + + // + return ( + + + // Axes + + + + // End Arrow + + // Axes arcs and arrows + + + + + + + + + + + + + + + + // Speed + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + // Pitch + {formats.f1(ship.calcPitch(eng, fuel, cargo, boost))}°/s + // Roll + {formats.f1(ship.calcRoll(eng, fuel, cargo, boost))}°/s + // Yaw + {formats.f1(ship.calcYaw(eng, fuel, cargo, boost))}°/s + + + ); + } +} diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 47b74b7f..e5b56dc0 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -173,3 +173,82 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res * (1 - (engpip * 1)), res]; } + +function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { + const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + const ynorm = Math.pow(xnorm, exponent); + const mul = minMul + ynorm * (maxMul - minMul); + const res = base * mul; + + return res * (1 - (engpip * (4 - eng))); +} + +export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index f1325dee..b8d11ade 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -194,6 +194,54 @@ export default class Ship { return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed); } + /** + * Calculate the speed for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Speed + */ + calcSpeed(eng, fuel, cargo, boost) { + return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the pitch for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Pitch + */ + calcPitch(eng, fuel, cargo, boost) { + return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the roll for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Roll + */ + calcRoll(eng, fuel, cargo, boost) { + return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the yaw for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Yaw + */ + calcYaw(eng, fuel, cargo, boost) { + return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + /** * Calculate the recovery time after losing or turning on shields * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas diff --git a/src/less/app.less b/src/less/app.less index 25d4de4a..22417f53 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -20,6 +20,7 @@ @import 'sortable'; @import 'loader'; @import 'pips'; +@import 'movement'; html, body { height: 100%; diff --git a/src/less/movement.less b/src/less/movement.less new file mode 100644 index 00000000..bfd43bac --- /dev/null +++ b/src/less/movement.less @@ -0,0 +1,26 @@ + +#movement { + svg { + width: 100%; + height: 100%; + stroke: @primary-disabled; + fill: @primary-disabled; + + text { + stroke: @primary; + } + } + + button { + font-size: 1.4em; + background: @primary-bg; + color: @primary; + border: 1px solid @primary; + &.boost { + // Shown when boost is enabled + background: @primary; + color: @primary-bg; + } + } + +} From 3a271e4b7b3fec8b44884e0f60c3a619a5f52ea9 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 17:57:03 +0000 Subject: [PATCH 03/53] Add ship picker --- src/app/components/BarChart.jsx | 2 +- src/app/components/BattleCentre.jsx | 32 +++-- src/app/components/Cargo.jsx | 2 +- src/app/components/CostSection.jsx | 2 +- src/app/components/DamageDealt.jsx | 2 +- src/app/components/DamageReceived.jsx | 2 +- src/app/components/DefenceSummary.jsx | 2 +- src/app/components/EngagementRange.jsx | 2 +- src/app/components/EngineProfile.jsx | 2 +- src/app/components/FSDProfile.jsx | 2 +- src/app/components/Fuel.jsx | 2 +- src/app/components/JumpRange.jsx | 2 +- src/app/components/LineChart.jsx | 2 +- src/app/components/Movement.jsx | 2 +- src/app/components/MovementSummary.jsx | 2 +- src/app/components/OffenceSummary.jsx | 2 +- src/app/components/Pips.jsx | 2 +- src/app/components/PowerManagement.jsx | 2 +- src/app/components/ShipPicker.jsx | 143 ++++++++++++++++++++ src/app/components/ShipSelector.jsx | 2 +- src/less/app.less | 1 + src/less/shippicker.less | 180 +++++++++++++++++++++++++ 22 files changed, 361 insertions(+), 31 deletions(-) create mode 100644 src/app/components/ShipPicker.jsx create mode 100755 src/less/shippicker.less diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx index 8ab59851..801e9f88 100644 --- a/src/app/components/BarChart.jsx +++ b/src/app/components/BarChart.jsx @@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent { unit: '' }; - static PropTypes = { + static propTypes = { colors: React.PropTypes.array, data: React.PropTypes.array.isRequired, desc: React.PropTypes.bool, diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index c5187ff5..88ce19eb 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -1,24 +1,23 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import Slider from '../components/Slider'; -import Pips from '../components/Pips'; -import Fuel from '../components/Fuel'; -import Cargo from '../components/Cargo'; -import Movement from '../components/Movement'; -import EngagementRange from '../components/EngagementRange'; +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'; /** * 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 = { + static propTypes = { ship: React.PropTypes.object.isRequired }; - static DEFAULT_OPPONENT = { ship: Ships['anaconda'] }; - /** * Constructor * @param {Object} props React Component properties @@ -34,18 +33,17 @@ export default class BattleCentre extends TranslatedComponent { this._fuelUpdated = this._fuelUpdated.bind(this); this._pipsUpdated = this._pipsUpdated.bind(this); this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); + this._targetShipUpdated = this._targetShipUpdated.bind(this); this.state = { // Pips sys: 2, eng: 2, wep: 2, - // Fuel fuel: ship.fuelCapacity, - // Cargo cargo: ship.cargoCapacity, - // Engagement range engagementRange: 1500, + targetShip: Ships['anaconda'] }; } @@ -83,6 +81,13 @@ export default class BattleCentre extends TranslatedComponent { this.setState({ engagementRange }); } + /** + * Triggered when target ship has been updated + */ + _targetShipUpdated(targetShip, targetBuild) { + this.setState({ targetShip, targetBuild: targetBuild }); + } + /** * Render * @return {React.Component} contents @@ -90,12 +95,13 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { sys, eng, wep, cargo, fuel, engagementRange, totals } = this.state; + const { sys, eng, wep, cargo, fuel, engagementRange } = this.state; const { ship } = this.props; return (

{translate('battle centre')}

+
diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index f03ae01e..25ec985a 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -8,7 +8,7 @@ import Slider from '../components/Slider'; * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change */ export default class Cargo extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 2df44855..f0dc5bfd 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class CostSection extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, buildName: React.PropTypes.string diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx index bb4bd484..c9bd714e 100644 --- a/src/app/components/DamageDealt.jsx +++ b/src/app/components/DamageDealt.jsx @@ -50,7 +50,7 @@ export function weaponComparator(translate, propComparator, desc) { * Damage against a selected ship */ export default class DamageDealt extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/DamageReceived.jsx b/src/app/components/DamageReceived.jsx index 294e4fbc..ec1799ed 100644 --- a/src/app/components/DamageReceived.jsx +++ b/src/app/components/DamageReceived.jsx @@ -45,7 +45,7 @@ export function weaponComparator(translate, propComparator, desc) { * Damage received by a selected ship */ export default class DamageReceived extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired }; diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx index 21896b90..ba6ac4e5 100644 --- a/src/app/components/DefenceSummary.jsx +++ b/src/app/components/DefenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; * Defence summary */ export default class DefenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index adfee2cd..c09b56ec 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -8,7 +8,7 @@ import Slider from '../components/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 { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index b8f874c3..0ff10850 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * Engine profile for a given ship */ export default class EngineProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index 1fa92289..b5138f9c 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * FSD profile for a given ship */ export default class FSDProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx index eb5d98c6..c3f9cc6b 100644 --- a/src/app/components/Fuel.jsx +++ b/src/app/components/Fuel.jsx @@ -8,7 +8,7 @@ import Slider from '../components/Slider'; * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change */ export default class Fuel extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/JumpRange.jsx b/src/app/components/JumpRange.jsx index d197ff04..e8929a58 100644 --- a/src/app/components/JumpRange.jsx +++ b/src/app/components/JumpRange.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * Jump range for a given ship */ export default class JumpRange extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 41021ff2..dbd18b16 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -17,7 +17,7 @@ export default class LineChart extends TranslatedComponent { colors: ['#ff8c0d'] }; - static PropTypes = { + static propTypes = { width: React.PropTypes.number.isRequired, func: React.PropTypes.func.isRequired, xLabel: React.PropTypes.string.isRequired, diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index 3cf1dc8e..f61843df 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent'; * Movement */ export default class Movement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, eng: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, diff --git a/src/app/components/MovementSummary.jsx b/src/app/components/MovementSummary.jsx index adc3974e..f4c268f1 100644 --- a/src/app/components/MovementSummary.jsx +++ b/src/app/components/MovementSummary.jsx @@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent'; * Movement summary */ export default class MovementSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx index 1aac8b8c..d7778a70 100644 --- a/src/app/components/OffenceSummary.jsx +++ b/src/app/components/OffenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive } from '. * Offence summary */ export default class OffenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index b6ecd59a..dfc7c7ce 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -14,7 +14,7 @@ import Module from '../shipyard/Module'; * 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 = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/PowerManagement.jsx b/src/app/components/PowerManagement.jsx index e8a81fd0..39e78a19 100644 --- a/src/app/components/PowerManagement.jsx +++ b/src/app/components/PowerManagement.jsx @@ -17,7 +17,7 @@ const POWER = [ * Power Management Section */ export default class PowerManagement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx new file mode 100644 index 00000000..49b67a2e --- /dev/null +++ b/src/app/components/ShipPicker.jsx @@ -0,0 +1,143 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Ship from '../shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import { Rocket } from './SvgIcons'; +import Persist from '../stores/Persist'; +import cn from 'classnames'; + +/** + * Ship picker + * Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change + */ +export default class ShipPicker extends TranslatedComponent { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + ship: React.PropTypes.object, + build: React.PropTypes.string + }; + + static defaultProps = { + ship: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots) + } + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.shipOrder = Object.keys(Ships).sort(); + 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; + } + + /** + * Update ship + * @param {object} ship the ship + */ + _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)); + } + this._closeMenu(); + this.setState({ ship, build }); + this.props.onChange(ship, build); + } + + /** + * Render the menu for the picker + */ + _renderPickerMenu() { + const { ship, build } = this.state; + const _shipChange = this._shipChange; + + const builds = Persist.getBuilds(); + const buildList = []; + for (let shipId of this.shipOrder) { + const shipBuilds = []; + // Add stock build + const stockSelected = (ship.id == shipId && !build); + shipBuilds.push(
  • Stock
  • ); + if (builds[shipId]) { + let buildNameOrder = Object.keys(builds[shipId]).sort(); + for (let buildName of buildNameOrder) { + const buildSelected = ship.id == shipId && build == buildName; + shipBuilds.push(
  • {buildName}
  • ); + } + } + buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); + } + + return buildList; + } + + /** + * Toggle the menu state + */ + _toggleMenu() { + const { menuOpen } = this.state; + this.setState({ menuOpen: !menuOpen }); + } + + /** + * Close the menu + */ + _closeMenu() { + const { menuOpen } = this.state; + if (menuOpen) { + this._toggleMenu(); + } + } + + /** + * Render picker + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { menuOpen, ship, build } = this.state; + + const shipString = ship.name + ': ' + (build ? build : 'stock'); + return ( +
    e.stopPropagation() }> +
    +
    + {shipString} +
    + { menuOpen ? +
    e.stopPropagation() }> +
    + {this._renderPickerMenu()} +
    +
    : null } +
    +
    + ); + } +} diff --git a/src/app/components/ShipSelector.jsx b/src/app/components/ShipSelector.jsx index 34f11786..a48ba4a8 100644 --- a/src/app/components/ShipSelector.jsx +++ b/src/app/components/ShipSelector.jsx @@ -8,7 +8,7 @@ import { Rocket } from './SvgIcons'; * Selector for ships */ export default class ShipSelector extends TranslatedComponent { - static PropTypes = { + static propTypes = { initial: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/less/app.less b/src/less/app.less index 22417f53..2af1c2f1 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -21,6 +21,7 @@ @import 'loader'; @import 'pips'; @import 'movement'; +@import 'shippicker'; html, body { height: 100%; diff --git a/src/less/shippicker.less b/src/less/shippicker.less new file mode 100755 index 00000000..11726fff --- /dev/null +++ b/src/less/shippicker.less @@ -0,0 +1,180 @@ +.shippicker { + background-color: @bgBlack; + margin: 0; + padding: 0 0 0 1em; + height: 3em; + line-height: 3em; + font-family: @fTitle; + vertical-align: middle; + position: relative; + + .user-select-none(); + + .menu { + position: relative; + z-index: 1; + cursor: default; + + &.r { + .menu-list { + right: 0; + } + } + + .smallTablet({ + position: static; + position: initial; + }); + } + + .menu-header { + height: 100%; + z-index: 2; + padding : 0 1em; + cursor: pointer; + color: @warning; + text-transform: uppercase; + // Less than 600px screen width: hide text + + &.disabled { + color: @warning-disabled; + cursor: default; + } + + &.selected { + background-color: @bgBlack; + } + + .menu-item-label { + margin-left: 1em; + display: inline-block; + + .smallTablet({ + display: none; + }); + } + } + + .menu-list { + font-family: @fStandard; + position: absolute; + padding: 0.5em 1em; + box-sizing: border-box; + min-width: 100%; + overflow-x: hidden; + background-color: @bgBlack; + font-size: 0.9em; + overflow-y: auto; + z-index: 0; + -webkit-overflow-scrolling: touch; + max-height: 500px; + + &::-webkit-scrollbar { + width: 0.5em; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: @warning-disabled; + } + + input { + border: none; + background-color: transparent; + text-align: right; + font-size: 1em; + font-family: @fStandard; + } + + .smallTablet({ + max-height: 400px; + left: 0; + right: 0; + border-bottom: 1px solid @bg; + }); + + .tablet({ + li, a { + padding: 0.3em 0; + } + }); + } + + .quad { + -webkit-column-count: 4; /* Chrome, Safari, Opera */ + -moz-column-count: 4; /* Firefox */ + column-count: 4; + ul { + min-width: 10em; + } + + .smallTablet({ + -webkit-column-count: 3; /* Chrome, Safari, Opera */ + -moz-column-count: 3; /* Firefox */ + column-count: 3; + + ul { + min-width: 20em; + } + }); + + .largePhone({ + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + }); + + .smallPhone({ + -webkit-column-count: 1; /* Chrome, Safari, Opera */ + -moz-column-count: 1; /* Firefox */ + column-count: 1; + }); + } + + ul { + display: inline-block; + white-space: nowrap; + margin: 0 0 0.5em; + padding: 0; + line-height: 1.3em; + color: @fg; + } + + li { + white-space: normal; + list-style: none; + margin-left: 1em; + line-height: 1.1em; + color: @warning; + cursor: pointer; + + &.selected { + color: @primary; + } + } + + hr { + border: none; + border-top: 1px solid @disabled; + } + + .no-wrap { + overflow-x: auto; + white-space: nowrap; + } + + .block { + display: block; + line-height: 1.5em; + } + + .title { + font-size: 1.3em; + display: inline-block; + margin:0px; + text-transform: uppercase; + } +} From ec4e70326a1793010b29965a6fe98ffc570340b4 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 22:22:12 +0000 Subject: [PATCH 04/53] Set initial chart width --- src/app/pages/OutfittingPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index fb0ffbd3..35c43532 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -85,6 +85,7 @@ export default class OutfittingPage extends Page { title: this._getTitle(buildName), costTab: Persist.getCostTab() || 'costs', buildName, + thirdChartWidth: 400, newBuildName: buildName, shipId, ship, From 340121c6bdb4cbc308f18b6a11a0898dedf83602 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 12 Mar 2017 17:00:04 +0000 Subject: [PATCH 05/53] Start of shields component --- .../components/.AvailableModulesMenu.jsx.swp | Bin 0 -> 16384 bytes src/app/components/.BattleCentre.jsx.swp | Bin 0 -> 20480 bytes src/app/components/BattleCentre.jsx | 38 +++- src/app/components/Cargo.jsx | 2 +- src/app/components/EngagementRange.jsx | 4 +- src/app/components/Movement.jsx | 10 +- src/app/components/Pips.jsx | 1 + src/app/components/Shields.jsx | 194 ++++++++++++++++++ src/app/components/ShipPicker.jsx | 4 +- src/app/shipyard/Calculations.js | 58 ++++++ src/app/shipyard/Ship.js | 6 +- 11 files changed, 296 insertions(+), 21 deletions(-) create mode 100644 src/app/components/.AvailableModulesMenu.jsx.swp create mode 100644 src/app/components/.BattleCentre.jsx.swp create mode 100644 src/app/components/Shields.jsx diff --git a/src/app/components/.AvailableModulesMenu.jsx.swp b/src/app/components/.AvailableModulesMenu.jsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..12d3cfc9e0afa0f8a5dfceedba27b76485a3e6a9 GIT binary patch literal 16384 zcmeI3TWlOx8ONuPUVxTMP#-`@oB-(^do%&CZO?$% z5So>KJ3Hro{^vVq=6o~T9KUydf$gu4Tll=yvMyfv?)^`H>y?{c+Gkm3TLFb$_j61H z>DaVIj8g?8bAc&)ohgsT~TC;_3H+{Ykxw+5N1s>-0 zv+2h5N-yI}1xf|3t-x*8UHkURss7&g>|t-;ed^lD%h*zZQh`!|Qh`!|Qh`!|Qh`!| zQi1;~1+sXT^$GOob-GuV^znwi<7@h}tivDZi+@$e=Q{kkzWCSc^C^8?&=r_^bZ~hn z6(|)b6(|)b6(|)b6(|)b6(|)b6(|)b6(|+>4=7-}mbDkv`}a@C5i8_!MXX7dYSym<3bd zm76W=AK(hu1}8uT+yZ`g6WRlw1W$lR!RNqdz^B0_kb()Y3%q)xWxWV~0v-lufd@D^ z1MUL<++|tc2Q6TOd%$k+EF3T21`mRZAOa?g#*TPs7E_0tdSv4pk8Oz!tt)FQNm(K{#tMWCx zfl@?QgHfhJ*yN$2y=hy-3wD~aF|>o-aR&?P0iAx({Fq?lwNYK|x}Ag~Y?;%*{KyhJ zOnp1!MF1M2e)~LQ9WkJrYGWs;-V~u*l@(B0(r=VGd6J;*Qb45>#ceTk!L)&ymX8)% z6xw1?CRv1?oJW@hdW_-W%7)Q!+q}9YZ5C#Zw2hGwvPor# zax7u9$Y%z2NA1)|^jc_<%b2V(21=yOHqB$6s9}&HP$A8d+{q#xy5r#dclDd@w5v-b z)n{Xd*p`WlLF;vpa?Jv~w`WP~m%~&^9Xkwpl2#_z>57w9Mp@vfTX3VraMyH0O~^d4 zeU@+$dQswVsFlf7ZQ0HsiA^8dg8Qy&UfQBMo9Ym&L+a?>Ltt3|4oaQGljk_7^B5z{ z!xm4NW8)g4ii{x-%8VVM4uLwHHysv1j0*{^rAg%brt^-HXoC{Bn<|UW(Z(gh)}!pH zL0r>OUe0~lBrl^Zo*70@s8FHPIf*>SK{ebCtH0mpNhf17VmD{bBv)468+w!l-3Yu;+m=So%O1EEr+k z=C1l+4-JOx4@(^k4;~(h*?= zHYoGJ{CdvgH99xutCGm^>aX0-2OkB9flahcm<3sD|sN5lrMyXyCx2%0vQG;_W}x6m2xguNsRSf$=rUTe)$ z)s+LL6erY?@@g5{0WKRws_ zSJRXsaorVr*b&SuE}V#5UbH`H9DD`PzW;8p7mR}ua69-b_Vs@TFM`Y9 z2jD?K`~DArTfr{yGWz}k_!W2tTn41a&p`Pp6(|)b6(|)b6(|)b75E=lV7udMEd1C9 z8?IP81V@DWY;~NFb#lBK7kMWIIm@S&2bcbQIk`1>V{0-|kx1+d=iKC0mDk8q&g+!E z>n^xj>+-`(PrO6Ugh|zs!u{S|ro6#Efg9|}Ehns2Qt7&f(?GTz+t@g*7fEU+<3aS( zq84dCGdaoO700$8Er6$Lh$mWk)(U^tHaiYQWJ);KD5rD?kW2_1;>zXP@2%Xl9baw; zrx_`bjxzeG)vRfktp2a-eQ-W3afgdFF4IYDWlG_Fc z)tv$Vcu^U2>CHy^cKf_H$B1x$m3xc~Fl$FXb7cl$Vzt{Pd>4pkh!OY+07UvdBCJ@i&>W7_?ND z2MM5y`iJq1aOGaG-zGYxU_S}}lFO>7Qe^OdV))Zq&&HGlkjeVZLYv;WYBL%)6R|ML zn4Q7j6lc^(<%nu-GTJ~QLiU?Uci||r$ytT4Aa~BCERUuAsT}lxAwi`H4QMf=bcK66 zBH8r>o6oSt4zZ^Gsm;GX)7Y?F2M2PY7fY%{*$^lr_mFMTYIAJNt5Y@VmLILlv)Nit z`^?Z6vr^YN@F1{AU|PlK`A*fYOP$%|1@(6vtTO=wInQozvo*7OTpXE1krbzf7S6ag_`~Du?_x9>OtQu)>==z>!oqkfw{sjghxS9X} literal 0 HcmV?d00001 diff --git a/src/app/components/.BattleCentre.jsx.swp b/src/app/components/.BattleCentre.jsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..a8be20232a793ea0242ccaddc4fa9cbafe6f54dc GIT binary patch literal 20480 zcmeI2Uu+yl9mlt6Na{dH)I7Wk^p-x6{$5l1-JV{bNq>));;&7w$I{=mQha);f3yNx0j+>mKr5gX z&Da@FMsQcnq|`aWD<0z)`Rr zyuHIPUIO0+kAiQ4uYoUtd%yyi1%H2!Vf+HDf>Yom_yBkYFJo>32f%)?6Z`=$V7?E& z2yO>Qz;)m^c(Lptv^roNjYmB}p3WR4RK2FYZz>%OzVhDM#X-3?-v+AQcGZmQ6nj<+Vv zZAgP6&tLQzvj*$;?Z|8o1_hqu42JY*S60$!5Lt3aXuwDuqPWN-e&G4E1u01w@o!TY zc39LY5vq_rG!o{{vkbHx33_3Tpcm9gm-#g!47o3D z8llweS=UJH+EFNSIp}>cN*&CjLQ#c#-gYgo3yJM7uo~gt3pBDll;?R)WTTN09s&O3 zt$NdF*j%za9*v^!MQ^+3*2TbBa6)nWl2a<|Rg zv{d;Z+cYH041uv6r%=VnhL%P*)v8jkYFsIJbhGlr1YH`V>{i<*EKRkjIDS^e#pv2) z=B-xAty)XJkEPWu;(cf29K zU@Bx>^@ETk%jisux_$K0m=Sx0P-Rub4$+Gqh;5i)n#-pF?GTdOwS0k@QJ=WzZ1a39 zL7^vIxJ=q^t8o^AVo+j1WV5tGN!veMF+NMr6vNxd-IUb*ZF8C+cGRRbi61$$Zrsz+?vIbVUip^Ix$f*M4TKG zC*=eq3~z?|)U>0XO2Dlnjpu*M(HK}K$q|(pRTZva0@;-qqUBiLBngoNxm>2GaU<=j zYCJEGFZE@NomHMi3_t0}wZS4ME2MPuX&oDsRj}WzfI`>?xwtIlwim)S$WSrwNH(je0(gDs zbLD)Z?jgBydzBey_*zG{3;}AOm)!v?7#NkD&E{d}OFf^3bx-uqx*aa${g)e*ZHg`@ z5<$qsVC%jYM8sk%GmOEU5*XmERtwRz9(g6KY+Eu^bbF6Yt8o zCCN#lCOUY?7@?$VTa2vHz-Cd8cq_zqNs9EBt0({D;+!He*|WPBxslaeo*juucrgO5k-!rQS%dD^o_EK&@m8 zY0{uP+3eshAuF#Go@ba#SAQ+*%&vUj4`I(kv5N|$JS~*+RYCOzSGGq3iwo%IiH6yRYT1yuJfrL{ zGHJk9WQgMX|EIBUe;x4sKeoc+^F!?OFM&1iK`;j1LYjYoH`4Tf<9YN)E1(t73TOqi z0$KsBfL1^&pcT*xXa%$aSDgag=lUs2XtI0ilrZUu+HHt;9x{V#({ z;38N9^MLySoCdSt2Jjlr`d(eWCg8pSJHR$@4R{OZ|IdMk!9!pH zjDuUi&EQ7xI{X2i1?wOHcYyt1FW3Xdz`x-G@H_AV_$6?`BzO%kI(`M72akd}_#V#l zzYD$rz5>pH8^J$uW`7y{7(5J4f!*L)X!aBk+Ff8I_b7u(iwVN<)r^( zPpNY2#kNnhnRdJwhv#^RBCLiZc(OFK$uiGY{hj!pQkfzDuS}5t-@x9K#3Cy95eF?82_*a-rK7?xemI1D$aHizW5)r&b;rrF&{uQeXw*qfXbCnxMw@ou=EF!!v z+UVo(yHH+X@D$}vo+h4F<}=2

    {translate('battle centre')}

    - +
    @@ -111,7 +128,10 @@ export default class BattleCentre extends TranslatedComponent {
    - + +
    +
    +
    ); diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index 25ec985a..bad1e2cf 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -25,7 +25,7 @@ export default class Cargo extends TranslatedComponent { this.state = { cargoCapacity: ship.cargoCapacity, - cargoLevel: 1, + cargoLevel: 0, }; } diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index c09b56ec..2f624bd0 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -26,7 +26,7 @@ export default class Range extends TranslatedComponent { const maxRange = this._calcMaxRange(ship); this.state = { - maxRange: maxRange, + maxRange, rangeLevel: 1, }; } @@ -75,7 +75,7 @@ export default class Range extends TranslatedComponent { /** * Update range - * @param {number} range percentage level from 0 to 1 + * @param {number} rangeLevel percentage level from 0 to 1 */ _rangeChange(rangeLevel) { const { maxRange } = this.state; diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index f61843df..a098ea2e 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -7,6 +7,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class Movement extends TranslatedComponent { static propTypes = { + marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, eng: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, @@ -44,7 +45,6 @@ export default class Movement extends TranslatedComponent { const { formats, translate, units } = language; const { boost } = this.state; - // return ( @@ -73,13 +73,13 @@ export default class Movement extends TranslatedComponent { // Speed {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s // Pitch - {formats.f1(ship.calcPitch(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s // Roll - {formats.f1(ship.calcRoll(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s // Yaw - {formats.f1(ship.calcYaw(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s - + { ship.canBoost() ? : null } ); } } diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index dfc7c7ce..8d123e82 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -242,6 +242,7 @@ export default class Pips extends TranslatedComponent { /** * Handle a click + * @param {string} which Which item was clicked */ onClick(which) { if (which == 'SYS') { diff --git a/src/app/components/Shields.jsx b/src/app/components/Shields.jsx new file mode 100644 index 00000000..376f6f88 --- /dev/null +++ b/src/app/components/Shields.jsx @@ -0,0 +1,194 @@ +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'; + +/** + * Shields + * Effective shield strength (including SCBs) + * Time for opponent to down shields + * - need sustained DPS for each type of damage (K/T/E/R) + * - turn in to % of shields removed per second + */ +export default class Shields extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + sys: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + const { shield, absolute, explosive, kinetic, thermal } = this._calcMetrics(props.ship, props.opponent, props.sys); + this.state = { shield, absolute, explosive, kinetic, thermal }; + } + + /** + * 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, absolute, explosive, kinetic, thermal } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); + this.setState({ shield, absolute, explosive, kinetic, thermal }); + return true; + } + } + + /** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The opponent ship + * @returns {Object} Shield metrics + */ + _calcMetrics(ship, opponent, sys) { + const sysResistance = this._calcSysResistance(sys); + + const shieldGenerator = ship.findShieldGenerator(); + + // 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; + boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; + boosterKinDmg = boosterKinDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterKinDmg) / 2; + boosterThermDmg = boosterThermDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterThermDmg) / 2; + + const generatorStrength = Calc.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1); + const boostersStrength = generatorStrength * boost; + const shield = { + generator: generatorStrength, + boosters: boostersStrength, + total: generatorStrength + boostersStrength + }; + + // Resistances have three components: the shield generator, the shield boosters and the SYS pips. + // We re-cast these as damage percentages + const absolute = { + generator: 1, + boosters: 1, + sys: 1 - sysResistance, + total: 1 - sysResistance + }; + + const explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance) + }; + + const kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance) + }; + + const thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance) + }; + + return { shield, absolute, explosive, kinetic, thermal }; + } + + /** + * 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, absolute, explosive, kinetic, thermal } = this.state; + + const shieldTooltipDetails = []; + shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); + shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); + + const absoluteTooltipDetails = []; + absoluteTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(absolute.generator)}
    ); + absoluteTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(absolute.boosters)}
    ); + absoluteTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(absolute.sys)}
    ); + + const explosiveTooltipDetails = []; + explosiveTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(explosive.generator)}
    ); + explosiveTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(explosive.boosters)}
    ); + explosiveTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(explosive.sys)}
    ); + + const kineticTooltipDetails = []; + kineticTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(kinetic.generator)}
    ); + kineticTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(kinetic.boosters)}
    ); + kineticTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(kinetic.sys)}
    ); + + const thermalTooltipDetails = []; + thermalTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(thermal.generator)}
    ); + thermalTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(thermal.boosters)}
    ); + thermalTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(thermal.sys)}
    ); + + return ( + + + + + + + + + + + + + + +
    {shieldTooltipDetails})} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ}
    {translate('damage from')} +   + {absoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(absolute.total)} + +   + {explosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(explosive.total)} + +   + {kineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(kinetic.total)} + +   + {thermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(thermal.total)} +
    +
    ); + } +} diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index 49b67a2e..9f319a14 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -56,7 +56,8 @@ export default class ShipPicker extends TranslatedComponent { /** * Update ship - * @param {object} ship the ship + * @param {object} shipId the ship + * @param {string} build the build, if present */ _shipChange(shipId, build) { const ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots); @@ -71,6 +72,7 @@ export default class ShipPicker extends TranslatedComponent { /** * Render the menu for the picker + * @returns {object} the picker menu */ _renderPickerMenu() { const { ship, build } = this.state; diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index e5b56dc0..7e74913b 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -174,6 +174,20 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res]; } +/** + * Calculate a single value + * @param {number} minMass the minimum mass of the thrusters + * @param {number} optMass the optimum mass of the thrusters + * @param {number} maxMass the maximum mass of the thrusters + * @param {number} minMul the minimum multiplier of the thrusters + * @param {number} optMul the optimum multiplier of the thrusters + * @param {number} maxMul the maximum multiplier of the thrusters + * @param {number} mass the mass of the ship + * @param {base} base the base value from which to calculate + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @returns {number} the resultant value + */ function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); @@ -184,6 +198,17 @@ function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base return res * (1 - (engpip * (4 - eng))); } +/** + * Calculate speed for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseSpeed the base speed of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant speed + */ export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -201,6 +226,17 @@ export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, return result; } +/** + * Calculate pitch for a given setup + * @param {number} mass the mass of the ship + * @param {number} basePitch the base pitch of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant pitch + */ export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -218,6 +254,17 @@ export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, return result; } +/** + * Calculate roll for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseRoll the base roll of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant roll + */ export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -235,6 +282,17 @@ export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, bo return result; } +/** + * Calculate yaw for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseYaw the base yaw of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant yaw + */ export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index b8d11ade..0caf79bd 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1286,13 +1286,13 @@ export default class Ship { shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); shieldExplDRStart = shieldExplRes * 0.7; - shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is + shieldExplDREnd = 0; shieldKinRes = 1 - sgSlot.m.getKineticResistance(); shieldKinDRStart = shieldKinRes * 0.7; - shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is + shieldKinDREnd = 0; shieldThermRes = 1 - sgSlot.m.getThermalResistance(); shieldThermDRStart = shieldThermRes * 0.7; - shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is + shieldThermDREnd = 0; // Shield from boosters for (let slot of this.hardpoints) { From 2731ec3b904766597c2f5295b053cb8cb44e3031 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 12 Mar 2017 17:00:19 +0000 Subject: [PATCH 06/53] Remove swap files --- .../components/.AvailableModulesMenu.jsx.swp | Bin 16384 -> 0 bytes src/app/components/.BattleCentre.jsx.swp | Bin 20480 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/app/components/.AvailableModulesMenu.jsx.swp delete mode 100644 src/app/components/.BattleCentre.jsx.swp diff --git a/src/app/components/.AvailableModulesMenu.jsx.swp b/src/app/components/.AvailableModulesMenu.jsx.swp deleted file mode 100644 index 12d3cfc9e0afa0f8a5dfceedba27b76485a3e6a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3TWlOx8ONuPUVxTMP#-`@oB-(^do%&CZO?$% z5So>KJ3Hro{^vVq=6o~T9KUydf$gu4Tll=yvMyfv?)^`H>y?{c+Gkm3TLFb$_j61H z>DaVIj8g?8bAc&)ohgsT~TC;_3H+{Ykxw+5N1s>-0 zv+2h5N-yI}1xf|3t-x*8UHkURss7&g>|t-;ed^lD%h*zZQh`!|Qh`!|Qh`!|Qh`!| zQi1;~1+sXT^$GOob-GuV^znwi<7@h}tivDZi+@$e=Q{kkzWCSc^C^8?&=r_^bZ~hn z6(|)b6(|)b6(|)b6(|)b6(|)b6(|)b6(|+>4=7-}mbDkv`}a@C5i8_!MXX7dYSym<3bd zm76W=AK(hu1}8uT+yZ`g6WRlw1W$lR!RNqdz^B0_kb()Y3%q)xWxWV~0v-lufd@D^ z1MUL<++|tc2Q6TOd%$k+EF3T21`mRZAOa?g#*TPs7E_0tdSv4pk8Oz!tt)FQNm(K{#tMWCx zfl@?QgHfhJ*yN$2y=hy-3wD~aF|>o-aR&?P0iAx({Fq?lwNYK|x}Ag~Y?;%*{KyhJ zOnp1!MF1M2e)~LQ9WkJrYGWs;-V~u*l@(B0(r=VGd6J;*Qb45>#ceTk!L)&ymX8)% z6xw1?CRv1?oJW@hdW_-W%7)Q!+q}9YZ5C#Zw2hGwvPor# zax7u9$Y%z2NA1)|^jc_<%b2V(21=yOHqB$6s9}&HP$A8d+{q#xy5r#dclDd@w5v-b z)n{Xd*p`WlLF;vpa?Jv~w`WP~m%~&^9Xkwpl2#_z>57w9Mp@vfTX3VraMyH0O~^d4 zeU@+$dQswVsFlf7ZQ0HsiA^8dg8Qy&UfQBMo9Ym&L+a?>Ltt3|4oaQGljk_7^B5z{ z!xm4NW8)g4ii{x-%8VVM4uLwHHysv1j0*{^rAg%brt^-HXoC{Bn<|UW(Z(gh)}!pH zL0r>OUe0~lBrl^Zo*70@s8FHPIf*>SK{ebCtH0mpNhf17VmD{bBv)468+w!l-3Yu;+m=So%O1EEr+k z=C1l+4-JOx4@(^k4;~(h*?= zHYoGJ{CdvgH99xutCGm^>aX0-2OkB9flahcm<3sD|sN5lrMyXyCx2%0vQG;_W}x6m2xguNsRSf$=rUTe)$ z)s+LL6erY?@@g5{0WKRws_ zSJRXsaorVr*b&SuE}V#5UbH`H9DD`PzW;8p7mR}ua69-b_Vs@TFM`Y9 z2jD?K`~DArTfr{yGWz}k_!W2tTn41a&p`Pp6(|)b6(|)b6(|)b75E=lV7udMEd1C9 z8?IP81V@DWY;~NFb#lBK7kMWIIm@S&2bcbQIk`1>V{0-|kx1+d=iKC0mDk8q&g+!E z>n^xj>+-`(PrO6Ugh|zs!u{S|ro6#Efg9|}Ehns2Qt7&f(?GTz+t@g*7fEU+<3aS( zq84dCGdaoO700$8Er6$Lh$mWk)(U^tHaiYQWJ);KD5rD?kW2_1;>zXP@2%Xl9baw; zrx_`bjxzeG)vRfktp2a-eQ-W3afgdFF4IYDWlG_Fc z)tv$Vcu^U2>CHy^cKf_H$B1x$m3xc~Fl$FXb7cl$Vzt{Pd>4pkh!OY+07UvdBCJ@i&>W7_?ND z2MM5y`iJq1aOGaG-zGYxU_S}}lFO>7Qe^OdV))Zq&&HGlkjeVZLYv;WYBL%)6R|ML zn4Q7j6lc^(<%nu-GTJ~QLiU?Uci||r$ytT4Aa~BCERUuAsT}lxAwi`H4QMf=bcK66 zBH8r>o6oSt4zZ^Gsm;GX)7Y?F2M2PY7fY%{*$^lr_mFMTYIAJNt5Y@VmLILlv)Nit z`^?Z6vr^YN@F1{AU|PlK`A*fYOP$%|1@(6vtTO=wInQozvo*7OTpXE1krbzf7S6ag_`~Du?_x9>OtQu)>==z>!oqkfw{sjghxS9X} diff --git a/src/app/components/.BattleCentre.jsx.swp b/src/app/components/.BattleCentre.jsx.swp deleted file mode 100644 index a8be20232a793ea0242ccaddc4fa9cbafe6f54dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI2Uu+yl9mlt6Na{dH)I7Wk^p-x6{$5l1-JV{bNq>));;&7w$I{=mQha);f3yNx0j+>mKr5gX z&Da@FMsQcnq|`aWD<0z)`Rr zyuHIPUIO0+kAiQ4uYoUtd%yyi1%H2!Vf+HDf>Yom_yBkYFJo>32f%)?6Z`=$V7?E& z2yO>Qz;)m^c(Lptv^roNjYmB}p3WR4RK2FYZz>%OzVhDM#X-3?-v+AQcGZmQ6nj<+Vv zZAgP6&tLQzvj*$;?Z|8o1_hqu42JY*S60$!5Lt3aXuwDuqPWN-e&G4E1u01w@o!TY zc39LY5vq_rG!o{{vkbHx33_3Tpcm9gm-#g!47o3D z8llweS=UJH+EFNSIp}>cN*&CjLQ#c#-gYgo3yJM7uo~gt3pBDll;?R)WTTN09s&O3 zt$NdF*j%za9*v^!MQ^+3*2TbBa6)nWl2a<|Rg zv{d;Z+cYH041uv6r%=VnhL%P*)v8jkYFsIJbhGlr1YH`V>{i<*EKRkjIDS^e#pv2) z=B-xAty)XJkEPWu;(cf29K zU@Bx>^@ETk%jisux_$K0m=Sx0P-Rub4$+Gqh;5i)n#-pF?GTdOwS0k@QJ=WzZ1a39 zL7^vIxJ=q^t8o^AVo+j1WV5tGN!veMF+NMr6vNxd-IUb*ZF8C+cGRRbi61$$Zrsz+?vIbVUip^Ix$f*M4TKG zC*=eq3~z?|)U>0XO2Dlnjpu*M(HK}K$q|(pRTZva0@;-qqUBiLBngoNxm>2GaU<=j zYCJEGFZE@NomHMi3_t0}wZS4ME2MPuX&oDsRj}WzfI`>?xwtIlwim)S$WSrwNH(je0(gDs zbLD)Z?jgBydzBey_*zG{3;}AOm)!v?7#NkD&E{d}OFf^3bx-uqx*aa${g)e*ZHg`@ z5<$qsVC%jYM8sk%GmOEU5*XmERtwRz9(g6KY+Eu^bbF6Yt8o zCCN#lCOUY?7@?$VTa2vHz-Cd8cq_zqNs9EBt0({D;+!He*|WPBxslaeo*juucrgO5k-!rQS%dD^o_EK&@m8 zY0{uP+3eshAuF#Go@ba#SAQ+*%&vUj4`I(kv5N|$JS~*+RYCOzSGGq3iwo%IiH6yRYT1yuJfrL{ zGHJk9WQgMX|EIBUe;x4sKeoc+^F!?OFM&1iK`;j1LYjYoH`4Tf<9YN)E1(t73TOqi z0$KsBfL1^&pcT*xXa%$aSDgag=lUs2XtI0ilrZUu+HHt;9x{V#({ z;38N9^MLySoCdSt2Jjlr`d(eWCg8pSJHR$@4R{OZ|IdMk!9!pH zjDuUi&EQ7xI{X2i1?wOHcYyt1FW3Xdz`x-G@H_AV_$6?`BzO%kI(`M72akd}_#V#l zzYD$rz5>pH8^J$uW`7y{7(5J4f!*L)X!aBk+Ff8I_b7u(iwVN<)r^( zPpNY2#kNnhnRdJwhv#^RBCLiZc(OFK$uiGY{hj!pQkfzDuS}5t-@x9K#3Cy95eF?82_*a-rK7?xemI1D$aHizW5)r&b;rrF&{uQeXw*qfXbCnxMw@ou=EF!!v z+UVo(yHH+X@D$}vo+h4F<}=2 Date: Sun, 12 Mar 2017 17:00:33 +0000 Subject: [PATCH 07/53] Ignore swap files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1bbc61e9..0dee7465 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ nginx.pid .idea /bin env +*.swp From 6f67267fecfc52913926bccead88e25483165a85 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 10 Mar 2017 17:17:37 +0000 Subject: [PATCH 08/53] Building components for battle centre --- src/app/components/BattleCentre.jsx | 98 +++++++ src/app/components/Cargo.jsx | 102 +++++++ src/app/components/EngagementRange.jsx | 123 +++++++++ src/app/components/Fuel.jsx | 96 +++++++ src/app/components/Pips.jsx | 358 +++++++++++++++++++++++++ src/app/components/SvgIcons.jsx | 18 ++ src/app/i18n/Language.jsx | 22 +- src/app/pages/OutfittingPage.jsx | 10 +- src/less/app.less | 1 + src/less/outfit.less | 8 + src/less/pips.less | 29 ++ 11 files changed, 846 insertions(+), 19 deletions(-) create mode 100644 src/app/components/BattleCentre.jsx create mode 100644 src/app/components/Cargo.jsx create mode 100644 src/app/components/EngagementRange.jsx create mode 100644 src/app/components/Fuel.jsx create mode 100644 src/app/components/Pips.jsx create mode 100755 src/less/pips.less diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx new file mode 100644 index 00000000..fc6297ea --- /dev/null +++ b/src/app/components/BattleCentre.jsx @@ -0,0 +1,98 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; +import Pips from '../components/Pips'; +import Fuel from '../components/Fuel'; +import Cargo from '../components/Cargo'; +import EngagementRange from '../components/EngagementRange'; + +/** + * 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 + }; + + static DEFAULT_OPPONENT = { ship: Ships['anaconda'] }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + const { ship } = this.props; + const opponent = BattleCentre.DEFAULT_OPPONENT; + + this.state = { }; + } + + 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 + */ + _pipsUpdated(sys, eng, wep) { + console.log('Pips are now ' + sys + '/' + eng + '/' + wep); + } + + /** + * Triggered when fuel has been updated + */ + _fuelUpdated(fuel) { + console.log('Fuel is now ' + fuel); + } + + /** + * Triggered when cargo has been updated + */ + _cargoUpdated(cargo) { + console.log('Cargo is now ' + cargo); + } + + /** + * Triggered when engagement range has been updated + */ + _engagementRangeUpdated(engagementRange) { + console.log('Engagement range is now ' + engagementRange); + } + + /** + * Render + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { against, expanded, maxRange, range, totals } = this.state; + const { ship } = this.props; + const shipUpdated = this._shipUpdated; + const pipsUpdated = this._pipsUpdated; + const fuelUpdated = this._fuelUpdated; + const cargoUpdated = this._cargoUpdated; + const engagementRangeUpdated = this._engagementRangeUpdated; + + return ( + +

    {translate('battle centre')}

    +
    + +
    +
    + + + +
    +
    + ); + } +} diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx new file mode 100644 index 00000000..1e7ddbaf --- /dev/null +++ b/src/app/components/Cargo.jsx @@ -0,0 +1,102 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Cargo slider + * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change + */ +export default class Cargo extends TranslatedComponent { + static PropTypes = { + 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 = this.props.ship; + + this.state = { + cargoCapacity: ship.cargoCapacity, + cargoLevel: 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 { 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; + } + + /** + * Update cargo level + * @param {number} cargoLevel percentage level from 0 to 1 + */ + _cargoChange(cargoLevel) { + const { cargoCapacity } = this.state; + // 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)); + } + } + + /** + * Render cargo slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { cargoLevel, cargoCapacity } = this.state; + + return ( + +

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

    + + + + + + +
    + +
    +
    + ); + } +} diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx new file mode 100644 index 00000000..adfee2cd --- /dev/null +++ b/src/app/components/EngagementRange.jsx @@ -0,0 +1,123 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +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 { + static PropTypes = { + 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 = this.props.ship; + + const maxRange = this._calcMaxRange(ship); + + this.state = { + maxRange: maxRange, + rangeLevel: 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 { 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 + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Update range + * @param {number} range percentage level from 0 to 1 + */ + _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)); + } + } + + /** + * Render range slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { rangeLevel, maxRange } = this.state; + + return ( + +

    {translate('engagement range')}: {formats.int(rangeLevel * maxRange)}{translate('m')}

    + + + + + + +
    + +
    +
    + ); + } +} diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx new file mode 100644 index 00000000..eb5d98c6 --- /dev/null +++ b/src/app/components/Fuel.jsx @@ -0,0 +1,96 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Slider from '../components/Slider'; + +/** + * Fuel slider + * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change + */ +export default class Fuel extends TranslatedComponent { + static PropTypes = { + 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 = 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; + } + + /** + * Update fuel level + * @param {number} fuelLevel percentage level from 0 to 1 + */ + _fuelChange(fuelLevel) { + this.setState({ fuelLevel }); + this.props.onChange(fuelLevel * this.state.fuelCapacity); + } + + /** + * Render fuel slider + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { fuelLevel, fuelCapacity } = this.state; + + return ( + +

    {translate('fuel carried')}: {formats.f2(fuelLevel * fuelCapacity)}{units.T}

    + + + + + + +
    + +
    +
    + ); + } +} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx new file mode 100644 index 00000000..b6ecd59a --- /dev/null +++ b/src/app/components/Pips.jsx @@ -0,0 +1,358 @@ +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'; + +/** + * 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) which is triggered whenever the pips change. + */ +export default class Pips extends TranslatedComponent { + static PropTypes = { + 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; + const pd = ship.standard[4].m; + + 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 + }; + } + + /** + * 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 { sysCap, engCap, wepCap, sysRate, engRate, wepRate } = this.state; + const ship = nextProps.ship; + const pd = ship.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 + */ + _keyDown(e) { + switch (e.keyCode) { + case 37: // Left arrow == increase SYS + e.preventDefault(); + this._incSys(); + break; + case 38: // Up arrow == increase ENG + e.preventDefault(); + this._incEng(); + break; + case 39: // Right arrow == increase WEP + e.preventDefault(); + this._incWep(); + break; + case 40: // Down arrow == reset + e.preventDefault(); + this._reset(); + break; + } + } + + /** + * Reset the capacitor + */ + _reset() { + 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); + } + } + + /** + * Increment the SYS capacitor + */ + _incSys() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - sys); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (eng > wep) { + eng -= 0.5; + sys += 0.5; + } else { + wep -= 0.5; + sys += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (eng == 0) { + wep -= 1; + sys += 1; + } else if (wep == 0) { + eng -= 1; + sys += 1; + } else { + eng -= 0.5; + wep -= 0.5; + sys += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the ENG capacitor + */ + _incEng() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - eng); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > wep) { + sys -= 0.5; + eng += 0.5; + } else { + wep -= 0.5; + eng += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + wep -= 1; + eng += 1; + } else if (wep == 0) { + sys -= 1; + eng += 1; + } else { + sys -= 0.5; + wep -= 0.5; + eng += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Increment the WEP capacitor + */ + _incWep() { + let { sys, eng, wep } = this.state; + + const required = Math.min(1, 4 - wep); + if (required > 0) { + if (required == 0.5) { + // Take from whichever is larger + if (sys > eng) { + sys -= 0.5; + wep += 0.5; + } else { + eng -= 0.5; + wep += 0.5; + } + } else { + // Required is 1 - take from both if possible + if (sys == 0) { + eng -= 1; + wep += 1; + } else if (eng == 0) { + sys -= 1; + wep += 1; + } else { + sys -= 0.5; + eng -= 0.5; + wep += 1; + } + } + this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); + this.props.onChange(sys, eng, wep); + } + } + + /** + * Handle a click + */ + onClick(which) { + if (which == 'SYS') { + this._incSys(); + } else if (which == 'ENG') { + this._incEng(); + } else if (which == 'WEP') { + this._incWep(); + } else if (which == 'RST') { + this._reset(); + } + } + + /** + * Set up the rendering for pips + * @param {int} sys the SYS pips + * @param {int} eng the ENG pips + * @param {int} wep the WEP pips + * @returns {Object} Object containing the rendering for the pips + */ + _renderPips(sys, eng, wep) { + const pipsSvg = {}; + + // SYS + pipsSvg['SYS'] = []; + for (let i = 0; i < Math.floor(sys); i++) { + pipsSvg['SYS'].push(); + } + if (sys > Math.floor(sys)) { + pipsSvg['SYS'].push(); + } + for (let i = Math.floor(sys + 0.5); i < 4; i++) { + pipsSvg['SYS'].push(); + } + + // ENG + pipsSvg['ENG'] = []; + for (let i = 0; i < Math.floor(eng); i++) { + pipsSvg['ENG'].push(); + } + if (eng > Math.floor(eng)) { + pipsSvg['ENG'].push(); + } + for (let i = Math.floor(eng + 0.5); i < 4; i++) { + pipsSvg['ENG'].push(); + } + + // WEP + pipsSvg['WEP'] = []; + for (let i = 0; i < Math.floor(wep); i++) { + pipsSvg['WEP'].push(); + } + if (wep > Math.floor(wep)) { + pipsSvg['WEP'].push(); + } + for (let i = Math.floor(wep + 0.5); i < 4; i++) { + pipsSvg['WEP'].push(); + } + + return pipsSvg; + } + + /** + * Render pips + * @return {React.Component} contents + */ + 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 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'); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      {pipsSvg['ENG']} 
     {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
     {translate('SYS')}{translate('RST')}{translate('WEP')}
    {translate('capacity')} ({units.MJ}){formats.f1(sysCap)}{formats.f1(engCap)}{formats.f1(wepCap)}
    {translate('recharge')} ({units.MW}){formats.f1(sysRate * (sys / 4))}{formats.f1(engRate * (eng / 4))}{formats.f1(wepRate * (wep / 4))}
    + ); + } +} diff --git a/src/app/components/SvgIcons.jsx b/src/app/components/SvgIcons.jsx index af119679..4593f173 100644 --- a/src/app/components/SvgIcons.jsx +++ b/src/app/components/SvgIcons.jsx @@ -708,6 +708,24 @@ export class Switch extends SvgIcon { } } +/** + * Pip + */ +export class Pip extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return ; + } +} + /** * In-game Coriolis Station logo */ diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index f22c0d42..e8d81da0 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -58,21 +58,21 @@ export function getLanguage(langCode) { }, translate, units: { - CR: {translate('CR')}, // Credits - kg: {translate('kg')}, // Kilograms - kgs: {translate('kg/s')}, // Kilograms per second - km: {translate('km')}, // Kilometers - Ls: {translate('Ls')}, // Light Seconds - LY: {translate('LY')}, // Light Years - MJ: {translate('MJ')}, // Mega Joules - 'm/s': {translate('m/s')}, // Meters per second - '°/s': {translate('°/s')}, // Degrees per second - MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) + CR: {translate('CR')}, // Credits + kg: {translate('kg')}, // Kilograms + kgs: {translate('kg/s')}, // Kilograms per second + km: {translate('km')}, // Kilometers + Ls: {translate('Ls')}, // Light Seconds + LY: {translate('LY')}, // Light Years + MJ: {translate('MJ')}, // Mega Joules + 'm/s': {translate('m/s')}, // Meters per second + '°/s': {translate('°/s')}, // Degrees per second + MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) mps: {translate('m/s')}, // Metres per second ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute s: {translate('secs')}, // Seconds - T: {translate('T')}, // Metric Tons + T: {translate('T')}, // Metric Tons } }; } diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 0e50fc63..fb0ffbd3 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -20,13 +20,11 @@ import MovementSummary from '../components/MovementSummary'; import EngineProfile from '../components/EngineProfile'; import FSDProfile from '../components/FSDProfile'; import JumpRange from '../components/JumpRange'; -import DamageDealt from '../components/DamageDealt'; -import DamageReceived from '../components/DamageReceived'; +import BattleCentre from '../components/BattleCentre'; import PowerManagement from '../components/PowerManagement'; import CostSection from '../components/CostSection'; import ModalExport from '../components/ModalExport'; import ModalPermalink from '../components/ModalPermalink'; -import Slider from '../components/Slider'; /** * Document Title Generator @@ -373,11 +371,7 @@ export default class OutfittingPage extends Page {
    - -
    - -
    - +
    diff --git a/src/less/app.less b/src/less/app.less index 24ade30c..303a3468 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -19,6 +19,7 @@ @import 'shipselector'; @import 'sortable'; @import 'loader'; +@import 'pips'; html, body { height: 100%; diff --git a/src/less/outfit.less b/src/less/outfit.less index 8d5e7f0e..7b792542 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -198,6 +198,14 @@ }); } + &.twothirds { + width: 67%; + + .smallTablet({ + width: 100% !important; + }); + } + .smallScreen({ .axis.x { g.tick:nth-child(2n + 1) text { diff --git a/src/less/pips.less b/src/less/pips.less new file mode 100755 index 00000000..d1b09f43 --- /dev/null +++ b/src/less/pips.less @@ -0,0 +1,29 @@ +// The pips table - keep the background black +.pipstable { + background-color: @bgBlack; + color: @primary; +} + +// A clickable entity in the pips table +.pipsclickable { + cursor: pointer; +} + +// A full pip +.fullpip { + stroke: @primary; + fill: @primary; +} + +// A half pip +.halfpip { + stroke: @primary-disabled; + fill: @primary-disabled; +} + +// An empty pip +.emptypip { + stroke: @primary-bg; + fill: @primary-bg; +} + From 067b69f44992b7dc6dcf30c4071b23a27febfc6b Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 13:28:24 +0000 Subject: [PATCH 09/53] Link in components --- src/app/components/BattleCentre.jsx | 45 ++++++++++----- src/app/components/Cargo.jsx | 12 ++-- src/app/components/Movement.jsx | 85 +++++++++++++++++++++++++++++ src/app/shipyard/Calculations.js | 79 +++++++++++++++++++++++++++ src/app/shipyard/Ship.js | 48 ++++++++++++++++ src/less/app.less | 1 + src/less/movement.less | 26 +++++++++ 7 files changed, 276 insertions(+), 20 deletions(-) create mode 100644 src/app/components/Movement.jsx create mode 100644 src/less/movement.less diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index fc6297ea..c5187ff5 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -5,6 +5,7 @@ import Slider from '../components/Slider'; import Pips from '../components/Pips'; import Fuel from '../components/Fuel'; import Cargo from '../components/Cargo'; +import Movement from '../components/Movement'; import EngagementRange from '../components/EngagementRange'; /** @@ -29,7 +30,23 @@ export default class BattleCentre extends TranslatedComponent { const { ship } = this.props; const opponent = BattleCentre.DEFAULT_OPPONENT; - this.state = { }; + 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.state = { + // Pips + sys: 2, + eng: 2, + wep: 2, + // Fuel + fuel: ship.fuelCapacity, + // Cargo + cargo: ship.cargoCapacity, + // Engagement range + engagementRange: 1500, + }; } componentWillReceiveProps(nextProps) { @@ -42,28 +59,28 @@ export default class BattleCentre extends TranslatedComponent { * Triggered when pips have been updated */ _pipsUpdated(sys, eng, wep) { - console.log('Pips are now ' + sys + '/' + eng + '/' + wep); + this.setState({ sys, eng, wep }); } /** * Triggered when fuel has been updated */ _fuelUpdated(fuel) { - console.log('Fuel is now ' + fuel); + this.setState({ fuel }); } /** * Triggered when cargo has been updated */ _cargoUpdated(cargo) { - console.log('Cargo is now ' + cargo); + this.setState({ cargo }); } /** * Triggered when engagement range has been updated */ _engagementRangeUpdated(engagementRange) { - console.log('Engagement range is now ' + engagementRange); + this.setState({ engagementRange }); } /** @@ -73,24 +90,22 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { against, expanded, maxRange, range, totals } = this.state; + const { sys, eng, wep, cargo, fuel, engagementRange, totals } = this.state; const { ship } = this.props; - const shipUpdated = this._shipUpdated; - const pipsUpdated = this._pipsUpdated; - const fuelUpdated = this._fuelUpdated; - const cargoUpdated = this._cargoUpdated; - const engagementRangeUpdated = this._engagementRangeUpdated; return (

    {translate('battle centre')}

    - +
    - - - + + + +
    +
    +
    ); diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index 1e7ddbaf..f03ae01e 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -58,12 +58,14 @@ export default class Cargo extends TranslatedComponent { */ _cargoChange(cargoLevel) { const { cargoCapacity } = this.state; - // We round the cargo level to a suitable value given the capacity - cargoLevel = Math.round(cargoLevel * cargoCapacity) / cargoCapacity; + 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)); + if (cargoLevel != this.state.cargoLevel) { + this.setState({ cargoLevel }); + this.props.onChange(Math.round(cargoLevel * cargoCapacity)); + } } } diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx new file mode 100644 index 00000000..3cf1dc8e --- /dev/null +++ b/src/app/components/Movement.jsx @@ -0,0 +1,85 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; + +/** + * Movement + */ +export default class Movement extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired, + eng: React.PropTypes.number.isRequired, + fuel: React.PropTypes.number.isRequired, + cargo: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._toggleBoost = this._toggleBoost.bind(this); + + this.state = { boost: false }; + } + + /** + * Toggle the boost feature + */ + _toggleBoost() { + let { boost } = this.state; + boost = !boost; + this.setState({ boost }); + } + + /** + * Render movement + * @return {React.Component} contents + */ + render() { + const { ship, eng, cargo, fuel } = this.props; + const { language } = this.context; + const { formats, translate, units } = language; + const { boost } = this.state; + + // + return ( + + + // Axes + + + + // End Arrow + + // Axes arcs and arrows + + + + + + + + + + + + + + + + // Speed + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + // Pitch + {formats.f1(ship.calcPitch(eng, fuel, cargo, boost))}°/s + // Roll + {formats.f1(ship.calcRoll(eng, fuel, cargo, boost))}°/s + // Yaw + {formats.f1(ship.calcYaw(eng, fuel, cargo, boost))}°/s + + + ); + } +} diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 47b74b7f..e5b56dc0 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -173,3 +173,82 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res * (1 - (engpip * 1)), res]; } + +function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { + const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + const ynorm = Math.pow(xnorm, exponent); + const mul = minMul + ynorm * (maxMul - minMul); + const res = base * mul; + + return res * (1 - (engpip * (4 - eng))); +} + +export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul); + const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + +export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { + // thrusters might be a module or a template; handle either here + let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; + let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass; + let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass; + let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul); + let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul); + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul); + + let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng); + if (boost == true) { + result *= boostFactor; + } + + return result; +} + diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 2370a295..a9b99652 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -194,6 +194,54 @@ export default class Ship { return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed); } + /** + * Calculate the speed for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Speed + */ + calcSpeed(eng, fuel, cargo, boost) { + return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the pitch for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Pitch + */ + calcPitch(eng, fuel, cargo, boost) { + return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the roll for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Roll + */ + calcRoll(eng, fuel, cargo, boost) { + return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + + /** + * Calculate the yaw for a given configuration + * @param {Number} eng Number of pips in ENG + * @param {Number} fuel Amount of fuel carried + * @param {Number} cargo Amount of cargo carried + * @param {boolean} boost true if boost is applied + * @return {Number} Yaw + */ + calcYaw(eng, fuel, cargo, boost) { + return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + } + /** * Calculate the recovery time after losing or turning on shields * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas diff --git a/src/less/app.less b/src/less/app.less index 303a3468..2dceb253 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -20,6 +20,7 @@ @import 'sortable'; @import 'loader'; @import 'pips'; +@import 'movement'; html, body { height: 100%; diff --git a/src/less/movement.less b/src/less/movement.less new file mode 100644 index 00000000..bfd43bac --- /dev/null +++ b/src/less/movement.less @@ -0,0 +1,26 @@ + +#movement { + svg { + width: 100%; + height: 100%; + stroke: @primary-disabled; + fill: @primary-disabled; + + text { + stroke: @primary; + } + } + + button { + font-size: 1.4em; + background: @primary-bg; + color: @primary; + border: 1px solid @primary; + &.boost { + // Shown when boost is enabled + background: @primary; + color: @primary-bg; + } + } + +} From 3b35d5030e4fbebca814b09603b44cd2349e96db Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 17:57:03 +0000 Subject: [PATCH 10/53] Add ship picker --- src/app/components/BarChart.jsx | 2 +- src/app/components/BattleCentre.jsx | 32 +++-- src/app/components/Cargo.jsx | 2 +- src/app/components/CostSection.jsx | 2 +- src/app/components/DamageDealt.jsx | 2 +- src/app/components/DamageReceived.jsx | 2 +- src/app/components/DefenceSummary.jsx | 2 +- src/app/components/EngagementRange.jsx | 2 +- src/app/components/EngineProfile.jsx | 2 +- src/app/components/FSDProfile.jsx | 2 +- src/app/components/Fuel.jsx | 2 +- src/app/components/JumpRange.jsx | 2 +- src/app/components/LineChart.jsx | 2 +- src/app/components/Movement.jsx | 2 +- src/app/components/MovementSummary.jsx | 2 +- src/app/components/OffenceSummary.jsx | 2 +- src/app/components/Pips.jsx | 2 +- src/app/components/PowerManagement.jsx | 2 +- src/app/components/ShipPicker.jsx | 143 ++++++++++++++++++++ src/app/components/ShipSelector.jsx | 2 +- src/less/app.less | 1 + src/less/shippicker.less | 180 +++++++++++++++++++++++++ 22 files changed, 361 insertions(+), 31 deletions(-) create mode 100644 src/app/components/ShipPicker.jsx create mode 100755 src/less/shippicker.less diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx index 8ab59851..801e9f88 100644 --- a/src/app/components/BarChart.jsx +++ b/src/app/components/BarChart.jsx @@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent { unit: '' }; - static PropTypes = { + static propTypes = { colors: React.PropTypes.array, data: React.PropTypes.array.isRequired, desc: React.PropTypes.bool, diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index c5187ff5..88ce19eb 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -1,24 +1,23 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import Slider from '../components/Slider'; -import Pips from '../components/Pips'; -import Fuel from '../components/Fuel'; -import Cargo from '../components/Cargo'; -import Movement from '../components/Movement'; -import EngagementRange from '../components/EngagementRange'; +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'; /** * 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 = { + static propTypes = { ship: React.PropTypes.object.isRequired }; - static DEFAULT_OPPONENT = { ship: Ships['anaconda'] }; - /** * Constructor * @param {Object} props React Component properties @@ -34,18 +33,17 @@ export default class BattleCentre extends TranslatedComponent { this._fuelUpdated = this._fuelUpdated.bind(this); this._pipsUpdated = this._pipsUpdated.bind(this); this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this); + this._targetShipUpdated = this._targetShipUpdated.bind(this); this.state = { // Pips sys: 2, eng: 2, wep: 2, - // Fuel fuel: ship.fuelCapacity, - // Cargo cargo: ship.cargoCapacity, - // Engagement range engagementRange: 1500, + targetShip: Ships['anaconda'] }; } @@ -83,6 +81,13 @@ export default class BattleCentre extends TranslatedComponent { this.setState({ engagementRange }); } + /** + * Triggered when target ship has been updated + */ + _targetShipUpdated(targetShip, targetBuild) { + this.setState({ targetShip, targetBuild: targetBuild }); + } + /** * Render * @return {React.Component} contents @@ -90,12 +95,13 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { sys, eng, wep, cargo, fuel, engagementRange, totals } = this.state; + const { sys, eng, wep, cargo, fuel, engagementRange } = this.state; const { ship } = this.props; return (

    {translate('battle centre')}

    +
    diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index f03ae01e..25ec985a 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -8,7 +8,7 @@ import Slider from '../components/Slider'; * Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change */ export default class Cargo extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 2df44855..f0dc5bfd 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class CostSection extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, buildName: React.PropTypes.string diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx index bb4bd484..c9bd714e 100644 --- a/src/app/components/DamageDealt.jsx +++ b/src/app/components/DamageDealt.jsx @@ -50,7 +50,7 @@ export function weaponComparator(translate, propComparator, desc) { * Damage against a selected ship */ export default class DamageDealt extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/DamageReceived.jsx b/src/app/components/DamageReceived.jsx index 294e4fbc..ec1799ed 100644 --- a/src/app/components/DamageReceived.jsx +++ b/src/app/components/DamageReceived.jsx @@ -45,7 +45,7 @@ export function weaponComparator(translate, propComparator, desc) { * Damage received by a selected ship */ export default class DamageReceived extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired }; diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx index 66cfd5de..93b230b0 100644 --- a/src/app/components/DefenceSummary.jsx +++ b/src/app/components/DefenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; * Defence summary */ export default class DefenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index adfee2cd..c09b56ec 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -8,7 +8,7 @@ import Slider from '../components/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 { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index b8f874c3..0ff10850 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * Engine profile for a given ship */ export default class EngineProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index 1fa92289..b5138f9c 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * FSD profile for a given ship */ export default class FSDProfile extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx index eb5d98c6..c3f9cc6b 100644 --- a/src/app/components/Fuel.jsx +++ b/src/app/components/Fuel.jsx @@ -8,7 +8,7 @@ import Slider from '../components/Slider'; * Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change */ export default class Fuel extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/JumpRange.jsx b/src/app/components/JumpRange.jsx index d197ff04..e8929a58 100644 --- a/src/app/components/JumpRange.jsx +++ b/src/app/components/JumpRange.jsx @@ -13,7 +13,7 @@ import * as Calc from '../shipyard/Calculations'; * Jump range for a given ship */ export default class JumpRange extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 41021ff2..dbd18b16 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -17,7 +17,7 @@ export default class LineChart extends TranslatedComponent { colors: ['#ff8c0d'] }; - static PropTypes = { + static propTypes = { width: React.PropTypes.number.isRequired, func: React.PropTypes.func.isRequired, xLabel: React.PropTypes.string.isRequired, diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index 3cf1dc8e..f61843df 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent'; * Movement */ export default class Movement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, eng: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, diff --git a/src/app/components/MovementSummary.jsx b/src/app/components/MovementSummary.jsx index adc3974e..f4c268f1 100644 --- a/src/app/components/MovementSummary.jsx +++ b/src/app/components/MovementSummary.jsx @@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent'; * Movement summary */ export default class MovementSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx index 1aac8b8c..d7778a70 100644 --- a/src/app/components/OffenceSummary.jsx +++ b/src/app/components/OffenceSummary.jsx @@ -7,7 +7,7 @@ import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive } from '. * Offence summary */ export default class OffenceSummary extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired }; diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index b6ecd59a..dfc7c7ce 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -14,7 +14,7 @@ import Module from '../shipyard/Module'; * 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 = { + static propTypes = { ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/app/components/PowerManagement.jsx b/src/app/components/PowerManagement.jsx index e8a81fd0..39e78a19 100644 --- a/src/app/components/PowerManagement.jsx +++ b/src/app/components/PowerManagement.jsx @@ -17,7 +17,7 @@ const POWER = [ * Power Management Section */ export default class PowerManagement extends TranslatedComponent { - static PropTypes = { + static propTypes = { ship: React.PropTypes.object.isRequired, code: React.PropTypes.string.isRequired, onChange: React.PropTypes.func.isRequired diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx new file mode 100644 index 00000000..49b67a2e --- /dev/null +++ b/src/app/components/ShipPicker.jsx @@ -0,0 +1,143 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Ship from '../shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import { Rocket } from './SvgIcons'; +import Persist from '../stores/Persist'; +import cn from 'classnames'; + +/** + * Ship picker + * Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change + */ +export default class ShipPicker extends TranslatedComponent { + static propTypes = { + onChange: React.PropTypes.func.isRequired, + ship: React.PropTypes.object, + build: React.PropTypes.string + }; + + static defaultProps = { + ship: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots) + } + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.shipOrder = Object.keys(Ships).sort(); + 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; + } + + /** + * Update ship + * @param {object} ship the ship + */ + _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)); + } + this._closeMenu(); + this.setState({ ship, build }); + this.props.onChange(ship, build); + } + + /** + * Render the menu for the picker + */ + _renderPickerMenu() { + const { ship, build } = this.state; + const _shipChange = this._shipChange; + + const builds = Persist.getBuilds(); + const buildList = []; + for (let shipId of this.shipOrder) { + const shipBuilds = []; + // Add stock build + const stockSelected = (ship.id == shipId && !build); + shipBuilds.push(
  • Stock
  • ); + if (builds[shipId]) { + let buildNameOrder = Object.keys(builds[shipId]).sort(); + for (let buildName of buildNameOrder) { + const buildSelected = ship.id == shipId && build == buildName; + shipBuilds.push(
  • {buildName}
  • ); + } + } + buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); + } + + return buildList; + } + + /** + * Toggle the menu state + */ + _toggleMenu() { + const { menuOpen } = this.state; + this.setState({ menuOpen: !menuOpen }); + } + + /** + * Close the menu + */ + _closeMenu() { + const { menuOpen } = this.state; + if (menuOpen) { + this._toggleMenu(); + } + } + + /** + * Render picker + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { menuOpen, ship, build } = this.state; + + const shipString = ship.name + ': ' + (build ? build : 'stock'); + return ( +
    e.stopPropagation() }> +
    +
    + {shipString} +
    + { menuOpen ? +
    e.stopPropagation() }> +
    + {this._renderPickerMenu()} +
    +
    : null } +
    +
    + ); + } +} diff --git a/src/app/components/ShipSelector.jsx b/src/app/components/ShipSelector.jsx index 34f11786..a48ba4a8 100644 --- a/src/app/components/ShipSelector.jsx +++ b/src/app/components/ShipSelector.jsx @@ -8,7 +8,7 @@ import { Rocket } from './SvgIcons'; * Selector for ships */ export default class ShipSelector extends TranslatedComponent { - static PropTypes = { + static propTypes = { initial: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; diff --git a/src/less/app.less b/src/less/app.less index 2dceb253..eb14b618 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -21,6 +21,7 @@ @import 'loader'; @import 'pips'; @import 'movement'; +@import 'shippicker'; html, body { height: 100%; diff --git a/src/less/shippicker.less b/src/less/shippicker.less new file mode 100755 index 00000000..11726fff --- /dev/null +++ b/src/less/shippicker.less @@ -0,0 +1,180 @@ +.shippicker { + background-color: @bgBlack; + margin: 0; + padding: 0 0 0 1em; + height: 3em; + line-height: 3em; + font-family: @fTitle; + vertical-align: middle; + position: relative; + + .user-select-none(); + + .menu { + position: relative; + z-index: 1; + cursor: default; + + &.r { + .menu-list { + right: 0; + } + } + + .smallTablet({ + position: static; + position: initial; + }); + } + + .menu-header { + height: 100%; + z-index: 2; + padding : 0 1em; + cursor: pointer; + color: @warning; + text-transform: uppercase; + // Less than 600px screen width: hide text + + &.disabled { + color: @warning-disabled; + cursor: default; + } + + &.selected { + background-color: @bgBlack; + } + + .menu-item-label { + margin-left: 1em; + display: inline-block; + + .smallTablet({ + display: none; + }); + } + } + + .menu-list { + font-family: @fStandard; + position: absolute; + padding: 0.5em 1em; + box-sizing: border-box; + min-width: 100%; + overflow-x: hidden; + background-color: @bgBlack; + font-size: 0.9em; + overflow-y: auto; + z-index: 0; + -webkit-overflow-scrolling: touch; + max-height: 500px; + + &::-webkit-scrollbar { + width: 0.5em; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: @warning-disabled; + } + + input { + border: none; + background-color: transparent; + text-align: right; + font-size: 1em; + font-family: @fStandard; + } + + .smallTablet({ + max-height: 400px; + left: 0; + right: 0; + border-bottom: 1px solid @bg; + }); + + .tablet({ + li, a { + padding: 0.3em 0; + } + }); + } + + .quad { + -webkit-column-count: 4; /* Chrome, Safari, Opera */ + -moz-column-count: 4; /* Firefox */ + column-count: 4; + ul { + min-width: 10em; + } + + .smallTablet({ + -webkit-column-count: 3; /* Chrome, Safari, Opera */ + -moz-column-count: 3; /* Firefox */ + column-count: 3; + + ul { + min-width: 20em; + } + }); + + .largePhone({ + -webkit-column-count: 2; /* Chrome, Safari, Opera */ + -moz-column-count: 2; /* Firefox */ + column-count: 2; + }); + + .smallPhone({ + -webkit-column-count: 1; /* Chrome, Safari, Opera */ + -moz-column-count: 1; /* Firefox */ + column-count: 1; + }); + } + + ul { + display: inline-block; + white-space: nowrap; + margin: 0 0 0.5em; + padding: 0; + line-height: 1.3em; + color: @fg; + } + + li { + white-space: normal; + list-style: none; + margin-left: 1em; + line-height: 1.1em; + color: @warning; + cursor: pointer; + + &.selected { + color: @primary; + } + } + + hr { + border: none; + border-top: 1px solid @disabled; + } + + .no-wrap { + overflow-x: auto; + white-space: nowrap; + } + + .block { + display: block; + line-height: 1.5em; + } + + .title { + font-size: 1.3em; + display: inline-block; + margin:0px; + text-transform: uppercase; + } +} From f6ebaf7445e4373bc4cd247f0f213e877851ead5 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 11 Mar 2017 22:22:12 +0000 Subject: [PATCH 11/53] Set initial chart width --- src/app/pages/OutfittingPage.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index fb0ffbd3..35c43532 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -85,6 +85,7 @@ export default class OutfittingPage extends Page { title: this._getTitle(buildName), costTab: Persist.getCostTab() || 'costs', buildName, + thirdChartWidth: 400, newBuildName: buildName, shipId, ship, From 950c0c61f9a0faccab6973acce44022b0d5ff12f Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 12 Mar 2017 17:00:04 +0000 Subject: [PATCH 12/53] Start of shields component --- .../components/.AvailableModulesMenu.jsx.swp | Bin 0 -> 16384 bytes src/app/components/.BattleCentre.jsx.swp | Bin 0 -> 20480 bytes src/app/components/BattleCentre.jsx | 38 +++- src/app/components/Cargo.jsx | 2 +- src/app/components/EngagementRange.jsx | 4 +- src/app/components/Movement.jsx | 10 +- src/app/components/Pips.jsx | 1 + src/app/components/Shields.jsx | 194 ++++++++++++++++++ src/app/components/ShipPicker.jsx | 4 +- src/app/shipyard/Calculations.js | 58 ++++++ src/app/shipyard/Ship.js | 6 +- 11 files changed, 296 insertions(+), 21 deletions(-) create mode 100644 src/app/components/.AvailableModulesMenu.jsx.swp create mode 100644 src/app/components/.BattleCentre.jsx.swp create mode 100644 src/app/components/Shields.jsx diff --git a/src/app/components/.AvailableModulesMenu.jsx.swp b/src/app/components/.AvailableModulesMenu.jsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..12d3cfc9e0afa0f8a5dfceedba27b76485a3e6a9 GIT binary patch literal 16384 zcmeI3TWlOx8ONuPUVxTMP#-`@oB-(^do%&CZO?$% z5So>KJ3Hro{^vVq=6o~T9KUydf$gu4Tll=yvMyfv?)^`H>y?{c+Gkm3TLFb$_j61H z>DaVIj8g?8bAc&)ohgsT~TC;_3H+{Ykxw+5N1s>-0 zv+2h5N-yI}1xf|3t-x*8UHkURss7&g>|t-;ed^lD%h*zZQh`!|Qh`!|Qh`!|Qh`!| zQi1;~1+sXT^$GOob-GuV^znwi<7@h}tivDZi+@$e=Q{kkzWCSc^C^8?&=r_^bZ~hn z6(|)b6(|)b6(|)b6(|)b6(|)b6(|)b6(|+>4=7-}mbDkv`}a@C5i8_!MXX7dYSym<3bd zm76W=AK(hu1}8uT+yZ`g6WRlw1W$lR!RNqdz^B0_kb()Y3%q)xWxWV~0v-lufd@D^ z1MUL<++|tc2Q6TOd%$k+EF3T21`mRZAOa?g#*TPs7E_0tdSv4pk8Oz!tt)FQNm(K{#tMWCx zfl@?QgHfhJ*yN$2y=hy-3wD~aF|>o-aR&?P0iAx({Fq?lwNYK|x}Ag~Y?;%*{KyhJ zOnp1!MF1M2e)~LQ9WkJrYGWs;-V~u*l@(B0(r=VGd6J;*Qb45>#ceTk!L)&ymX8)% z6xw1?CRv1?oJW@hdW_-W%7)Q!+q}9YZ5C#Zw2hGwvPor# zax7u9$Y%z2NA1)|^jc_<%b2V(21=yOHqB$6s9}&HP$A8d+{q#xy5r#dclDd@w5v-b z)n{Xd*p`WlLF;vpa?Jv~w`WP~m%~&^9Xkwpl2#_z>57w9Mp@vfTX3VraMyH0O~^d4 zeU@+$dQswVsFlf7ZQ0HsiA^8dg8Qy&UfQBMo9Ym&L+a?>Ltt3|4oaQGljk_7^B5z{ z!xm4NW8)g4ii{x-%8VVM4uLwHHysv1j0*{^rAg%brt^-HXoC{Bn<|UW(Z(gh)}!pH zL0r>OUe0~lBrl^Zo*70@s8FHPIf*>SK{ebCtH0mpNhf17VmD{bBv)468+w!l-3Yu;+m=So%O1EEr+k z=C1l+4-JOx4@(^k4;~(h*?= zHYoGJ{CdvgH99xutCGm^>aX0-2OkB9flahcm<3sD|sN5lrMyXyCx2%0vQG;_W}x6m2xguNsRSf$=rUTe)$ z)s+LL6erY?@@g5{0WKRws_ zSJRXsaorVr*b&SuE}V#5UbH`H9DD`PzW;8p7mR}ua69-b_Vs@TFM`Y9 z2jD?K`~DArTfr{yGWz}k_!W2tTn41a&p`Pp6(|)b6(|)b6(|)b75E=lV7udMEd1C9 z8?IP81V@DWY;~NFb#lBK7kMWIIm@S&2bcbQIk`1>V{0-|kx1+d=iKC0mDk8q&g+!E z>n^xj>+-`(PrO6Ugh|zs!u{S|ro6#Efg9|}Ehns2Qt7&f(?GTz+t@g*7fEU+<3aS( zq84dCGdaoO700$8Er6$Lh$mWk)(U^tHaiYQWJ);KD5rD?kW2_1;>zXP@2%Xl9baw; zrx_`bjxzeG)vRfktp2a-eQ-W3afgdFF4IYDWlG_Fc z)tv$Vcu^U2>CHy^cKf_H$B1x$m3xc~Fl$FXb7cl$Vzt{Pd>4pkh!OY+07UvdBCJ@i&>W7_?ND z2MM5y`iJq1aOGaG-zGYxU_S}}lFO>7Qe^OdV))Zq&&HGlkjeVZLYv;WYBL%)6R|ML zn4Q7j6lc^(<%nu-GTJ~QLiU?Uci||r$ytT4Aa~BCERUuAsT}lxAwi`H4QMf=bcK66 zBH8r>o6oSt4zZ^Gsm;GX)7Y?F2M2PY7fY%{*$^lr_mFMTYIAJNt5Y@VmLILlv)Nit z`^?Z6vr^YN@F1{AU|PlK`A*fYOP$%|1@(6vtTO=wInQozvo*7OTpXE1krbzf7S6ag_`~Du?_x9>OtQu)>==z>!oqkfw{sjghxS9X} literal 0 HcmV?d00001 diff --git a/src/app/components/.BattleCentre.jsx.swp b/src/app/components/.BattleCentre.jsx.swp new file mode 100644 index 0000000000000000000000000000000000000000..a8be20232a793ea0242ccaddc4fa9cbafe6f54dc GIT binary patch literal 20480 zcmeI2Uu+yl9mlt6Na{dH)I7Wk^p-x6{$5l1-JV{bNq>));;&7w$I{=mQha);f3yNx0j+>mKr5gX z&Da@FMsQcnq|`aWD<0z)`Rr zyuHIPUIO0+kAiQ4uYoUtd%yyi1%H2!Vf+HDf>Yom_yBkYFJo>32f%)?6Z`=$V7?E& z2yO>Qz;)m^c(Lptv^roNjYmB}p3WR4RK2FYZz>%OzVhDM#X-3?-v+AQcGZmQ6nj<+Vv zZAgP6&tLQzvj*$;?Z|8o1_hqu42JY*S60$!5Lt3aXuwDuqPWN-e&G4E1u01w@o!TY zc39LY5vq_rG!o{{vkbHx33_3Tpcm9gm-#g!47o3D z8llweS=UJH+EFNSIp}>cN*&CjLQ#c#-gYgo3yJM7uo~gt3pBDll;?R)WTTN09s&O3 zt$NdF*j%za9*v^!MQ^+3*2TbBa6)nWl2a<|Rg zv{d;Z+cYH041uv6r%=VnhL%P*)v8jkYFsIJbhGlr1YH`V>{i<*EKRkjIDS^e#pv2) z=B-xAty)XJkEPWu;(cf29K zU@Bx>^@ETk%jisux_$K0m=Sx0P-Rub4$+Gqh;5i)n#-pF?GTdOwS0k@QJ=WzZ1a39 zL7^vIxJ=q^t8o^AVo+j1WV5tGN!veMF+NMr6vNxd-IUb*ZF8C+cGRRbi61$$Zrsz+?vIbVUip^Ix$f*M4TKG zC*=eq3~z?|)U>0XO2Dlnjpu*M(HK}K$q|(pRTZva0@;-qqUBiLBngoNxm>2GaU<=j zYCJEGFZE@NomHMi3_t0}wZS4ME2MPuX&oDsRj}WzfI`>?xwtIlwim)S$WSrwNH(je0(gDs zbLD)Z?jgBydzBey_*zG{3;}AOm)!v?7#NkD&E{d}OFf^3bx-uqx*aa${g)e*ZHg`@ z5<$qsVC%jYM8sk%GmOEU5*XmERtwRz9(g6KY+Eu^bbF6Yt8o zCCN#lCOUY?7@?$VTa2vHz-Cd8cq_zqNs9EBt0({D;+!He*|WPBxslaeo*juucrgO5k-!rQS%dD^o_EK&@m8 zY0{uP+3eshAuF#Go@ba#SAQ+*%&vUj4`I(kv5N|$JS~*+RYCOzSGGq3iwo%IiH6yRYT1yuJfrL{ zGHJk9WQgMX|EIBUe;x4sKeoc+^F!?OFM&1iK`;j1LYjYoH`4Tf<9YN)E1(t73TOqi z0$KsBfL1^&pcT*xXa%$aSDgag=lUs2XtI0ilrZUu+HHt;9x{V#({ z;38N9^MLySoCdSt2Jjlr`d(eWCg8pSJHR$@4R{OZ|IdMk!9!pH zjDuUi&EQ7xI{X2i1?wOHcYyt1FW3Xdz`x-G@H_AV_$6?`BzO%kI(`M72akd}_#V#l zzYD$rz5>pH8^J$uW`7y{7(5J4f!*L)X!aBk+Ff8I_b7u(iwVN<)r^( zPpNY2#kNnhnRdJwhv#^RBCLiZc(OFK$uiGY{hj!pQkfzDuS}5t-@x9K#3Cy95eF?82_*a-rK7?xemI1D$aHizW5)r&b;rrF&{uQeXw*qfXbCnxMw@ou=EF!!v z+UVo(yHH+X@D$}vo+h4F<}=2

    {translate('battle centre')}

    - +
    @@ -111,7 +128,10 @@ export default class BattleCentre extends TranslatedComponent {
    - + +
    +
    +
    ); diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index 25ec985a..bad1e2cf 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -25,7 +25,7 @@ export default class Cargo extends TranslatedComponent { this.state = { cargoCapacity: ship.cargoCapacity, - cargoLevel: 1, + cargoLevel: 0, }; } diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index c09b56ec..2f624bd0 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -26,7 +26,7 @@ export default class Range extends TranslatedComponent { const maxRange = this._calcMaxRange(ship); this.state = { - maxRange: maxRange, + maxRange, rangeLevel: 1, }; } @@ -75,7 +75,7 @@ export default class Range extends TranslatedComponent { /** * Update range - * @param {number} range percentage level from 0 to 1 + * @param {number} rangeLevel percentage level from 0 to 1 */ _rangeChange(rangeLevel) { const { maxRange } = this.state; diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index f61843df..a098ea2e 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -7,6 +7,7 @@ import TranslatedComponent from './TranslatedComponent'; */ export default class Movement extends TranslatedComponent { static propTypes = { + marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, eng: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, @@ -44,7 +45,6 @@ export default class Movement extends TranslatedComponent { const { formats, translate, units } = language; const { boost } = this.state; - // return ( @@ -73,13 +73,13 @@ export default class Movement extends TranslatedComponent { // Speed {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s // Pitch - {formats.f1(ship.calcPitch(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s // Roll - {formats.f1(ship.calcRoll(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s // Yaw - {formats.f1(ship.calcYaw(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s - + { ship.canBoost() ? : null } ); } } diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index dfc7c7ce..8d123e82 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -242,6 +242,7 @@ export default class Pips extends TranslatedComponent { /** * Handle a click + * @param {string} which Which item was clicked */ onClick(which) { if (which == 'SYS') { diff --git a/src/app/components/Shields.jsx b/src/app/components/Shields.jsx new file mode 100644 index 00000000..376f6f88 --- /dev/null +++ b/src/app/components/Shields.jsx @@ -0,0 +1,194 @@ +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'; + +/** + * Shields + * Effective shield strength (including SCBs) + * Time for opponent to down shields + * - need sustained DPS for each type of damage (K/T/E/R) + * - turn in to % of shields removed per second + */ +export default class Shields extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + sys: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + const { shield, absolute, explosive, kinetic, thermal } = this._calcMetrics(props.ship, props.opponent, props.sys); + this.state = { shield, absolute, explosive, kinetic, thermal }; + } + + /** + * 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, absolute, explosive, kinetic, thermal } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); + this.setState({ shield, absolute, explosive, kinetic, thermal }); + return true; + } + } + + /** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The opponent ship + * @returns {Object} Shield metrics + */ + _calcMetrics(ship, opponent, sys) { + const sysResistance = this._calcSysResistance(sys); + + const shieldGenerator = ship.findShieldGenerator(); + + // 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; + boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; + boosterKinDmg = boosterKinDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterKinDmg) / 2; + boosterThermDmg = boosterThermDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterThermDmg) / 2; + + const generatorStrength = Calc.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1); + const boostersStrength = generatorStrength * boost; + const shield = { + generator: generatorStrength, + boosters: boostersStrength, + total: generatorStrength + boostersStrength + }; + + // Resistances have three components: the shield generator, the shield boosters and the SYS pips. + // We re-cast these as damage percentages + const absolute = { + generator: 1, + boosters: 1, + sys: 1 - sysResistance, + total: 1 - sysResistance + }; + + const explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance) + }; + + const kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance) + }; + + const thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance) + }; + + return { shield, absolute, explosive, kinetic, thermal }; + } + + /** + * 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, absolute, explosive, kinetic, thermal } = this.state; + + const shieldTooltipDetails = []; + shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); + shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); + + const absoluteTooltipDetails = []; + absoluteTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(absolute.generator)}
    ); + absoluteTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(absolute.boosters)}
    ); + absoluteTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(absolute.sys)}
    ); + + const explosiveTooltipDetails = []; + explosiveTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(explosive.generator)}
    ); + explosiveTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(explosive.boosters)}
    ); + explosiveTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(explosive.sys)}
    ); + + const kineticTooltipDetails = []; + kineticTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(kinetic.generator)}
    ); + kineticTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(kinetic.boosters)}
    ); + kineticTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(kinetic.sys)}
    ); + + const thermalTooltipDetails = []; + thermalTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(thermal.generator)}
    ); + thermalTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(thermal.boosters)}
    ); + thermalTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(thermal.sys)}
    ); + + return ( + + + + + + + + + + + + + + +
    {shieldTooltipDetails})} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ}
    {translate('damage from')} +   + {absoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(absolute.total)} + +   + {explosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(explosive.total)} + +   + {kineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(kinetic.total)} + +   + {thermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(thermal.total)} +
    +
    ); + } +} diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index 49b67a2e..9f319a14 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -56,7 +56,8 @@ export default class ShipPicker extends TranslatedComponent { /** * Update ship - * @param {object} ship the ship + * @param {object} shipId the ship + * @param {string} build the build, if present */ _shipChange(shipId, build) { const ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots); @@ -71,6 +72,7 @@ export default class ShipPicker extends TranslatedComponent { /** * Render the menu for the picker + * @returns {object} the picker menu */ _renderPickerMenu() { const { ship, build } = this.state; diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index e5b56dc0..7e74913b 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -174,6 +174,20 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas res]; } +/** + * Calculate a single value + * @param {number} minMass the minimum mass of the thrusters + * @param {number} optMass the optimum mass of the thrusters + * @param {number} maxMass the maximum mass of the thrusters + * @param {number} minMul the minimum multiplier of the thrusters + * @param {number} optMul the optimum multiplier of the thrusters + * @param {number} maxMul the maximum multiplier of the thrusters + * @param {number} mass the mass of the ship + * @param {base} base the base value from which to calculate + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @returns {number} the resultant value + */ function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) { const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); @@ -184,6 +198,17 @@ function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base return res * (1 - (engpip * (4 - eng))); } +/** + * Calculate speed for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseSpeed the base speed of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant speed + */ export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -201,6 +226,17 @@ export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, return result; } +/** + * Calculate pitch for a given setup + * @param {number} mass the mass of the ship + * @param {number} basePitch the base pitch of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant pitch + */ export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -218,6 +254,17 @@ export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, return result; } +/** + * Calculate roll for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseRoll the base roll of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant roll + */ export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; @@ -235,6 +282,17 @@ export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, bo return result; } +/** + * Calculate yaw for a given setup + * @param {number} mass the mass of the ship + * @param {number} baseYaw the base yaw of the ship + * @param {ojbect} thrusters the thrusters of the ship + * @param {number} engpip the multiplier per pip to engines + * @param {number} eng the pips to engines + * @param {number} boostFactor the boost factor for ths ship + * @param {boolean} boost true if the boost is activated + * @returns {number} the resultant yaw + */ export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) { // thrusters might be a module or a template; handle either here let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index a9b99652..10da4582 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1286,13 +1286,13 @@ export default class Ship { shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); shieldExplDRStart = shieldExplRes * 0.7; - shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is + shieldExplDREnd = 0; shieldKinRes = 1 - sgSlot.m.getKineticResistance(); shieldKinDRStart = shieldKinRes * 0.7; - shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is + shieldKinDREnd = 0; shieldThermRes = 1 - sgSlot.m.getThermalResistance(); shieldThermDRStart = shieldThermRes * 0.7; - shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is + shieldThermDREnd = 0; // Shield from boosters for (let slot of this.hardpoints) { From f782adb21d31162e7f84c22ba45a4360be4da21e Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 12 Mar 2017 17:00:19 +0000 Subject: [PATCH 13/53] Remove swap files --- .../components/.AvailableModulesMenu.jsx.swp | Bin 16384 -> 0 bytes src/app/components/.BattleCentre.jsx.swp | Bin 20480 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/app/components/.AvailableModulesMenu.jsx.swp delete mode 100644 src/app/components/.BattleCentre.jsx.swp diff --git a/src/app/components/.AvailableModulesMenu.jsx.swp b/src/app/components/.AvailableModulesMenu.jsx.swp deleted file mode 100644 index 12d3cfc9e0afa0f8a5dfceedba27b76485a3e6a9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI3TWlOx8ONuPUVxTMP#-`@oB-(^do%&CZO?$% z5So>KJ3Hro{^vVq=6o~T9KUydf$gu4Tll=yvMyfv?)^`H>y?{c+Gkm3TLFb$_j61H z>DaVIj8g?8bAc&)ohgsT~TC;_3H+{Ykxw+5N1s>-0 zv+2h5N-yI}1xf|3t-x*8UHkURss7&g>|t-;ed^lD%h*zZQh`!|Qh`!|Qh`!|Qh`!| zQi1;~1+sXT^$GOob-GuV^znwi<7@h}tivDZi+@$e=Q{kkzWCSc^C^8?&=r_^bZ~hn z6(|)b6(|)b6(|)b6(|)b6(|)b6(|)b6(|+>4=7-}mbDkv`}a@C5i8_!MXX7dYSym<3bd zm76W=AK(hu1}8uT+yZ`g6WRlw1W$lR!RNqdz^B0_kb()Y3%q)xWxWV~0v-lufd@D^ z1MUL<++|tc2Q6TOd%$k+EF3T21`mRZAOa?g#*TPs7E_0tdSv4pk8Oz!tt)FQNm(K{#tMWCx zfl@?QgHfhJ*yN$2y=hy-3wD~aF|>o-aR&?P0iAx({Fq?lwNYK|x}Ag~Y?;%*{KyhJ zOnp1!MF1M2e)~LQ9WkJrYGWs;-V~u*l@(B0(r=VGd6J;*Qb45>#ceTk!L)&ymX8)% z6xw1?CRv1?oJW@hdW_-W%7)Q!+q}9YZ5C#Zw2hGwvPor# zax7u9$Y%z2NA1)|^jc_<%b2V(21=yOHqB$6s9}&HP$A8d+{q#xy5r#dclDd@w5v-b z)n{Xd*p`WlLF;vpa?Jv~w`WP~m%~&^9Xkwpl2#_z>57w9Mp@vfTX3VraMyH0O~^d4 zeU@+$dQswVsFlf7ZQ0HsiA^8dg8Qy&UfQBMo9Ym&L+a?>Ltt3|4oaQGljk_7^B5z{ z!xm4NW8)g4ii{x-%8VVM4uLwHHysv1j0*{^rAg%brt^-HXoC{Bn<|UW(Z(gh)}!pH zL0r>OUe0~lBrl^Zo*70@s8FHPIf*>SK{ebCtH0mpNhf17VmD{bBv)468+w!l-3Yu;+m=So%O1EEr+k z=C1l+4-JOx4@(^k4;~(h*?= zHYoGJ{CdvgH99xutCGm^>aX0-2OkB9flahcm<3sD|sN5lrMyXyCx2%0vQG;_W}x6m2xguNsRSf$=rUTe)$ z)s+LL6erY?@@g5{0WKRws_ zSJRXsaorVr*b&SuE}V#5UbH`H9DD`PzW;8p7mR}ua69-b_Vs@TFM`Y9 z2jD?K`~DArTfr{yGWz}k_!W2tTn41a&p`Pp6(|)b6(|)b6(|)b75E=lV7udMEd1C9 z8?IP81V@DWY;~NFb#lBK7kMWIIm@S&2bcbQIk`1>V{0-|kx1+d=iKC0mDk8q&g+!E z>n^xj>+-`(PrO6Ugh|zs!u{S|ro6#Efg9|}Ehns2Qt7&f(?GTz+t@g*7fEU+<3aS( zq84dCGdaoO700$8Er6$Lh$mWk)(U^tHaiYQWJ);KD5rD?kW2_1;>zXP@2%Xl9baw; zrx_`bjxzeG)vRfktp2a-eQ-W3afgdFF4IYDWlG_Fc z)tv$Vcu^U2>CHy^cKf_H$B1x$m3xc~Fl$FXb7cl$Vzt{Pd>4pkh!OY+07UvdBCJ@i&>W7_?ND z2MM5y`iJq1aOGaG-zGYxU_S}}lFO>7Qe^OdV))Zq&&HGlkjeVZLYv;WYBL%)6R|ML zn4Q7j6lc^(<%nu-GTJ~QLiU?Uci||r$ytT4Aa~BCERUuAsT}lxAwi`H4QMf=bcK66 zBH8r>o6oSt4zZ^Gsm;GX)7Y?F2M2PY7fY%{*$^lr_mFMTYIAJNt5Y@VmLILlv)Nit z`^?Z6vr^YN@F1{AU|PlK`A*fYOP$%|1@(6vtTO=wInQozvo*7OTpXE1krbzf7S6ag_`~Du?_x9>OtQu)>==z>!oqkfw{sjghxS9X} diff --git a/src/app/components/.BattleCentre.jsx.swp b/src/app/components/.BattleCentre.jsx.swp deleted file mode 100644 index a8be20232a793ea0242ccaddc4fa9cbafe6f54dc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 zcmeI2Uu+yl9mlt6Na{dH)I7Wk^p-x6{$5l1-JV{bNq>));;&7w$I{=mQha);f3yNx0j+>mKr5gX z&Da@FMsQcnq|`aWD<0z)`Rr zyuHIPUIO0+kAiQ4uYoUtd%yyi1%H2!Vf+HDf>Yom_yBkYFJo>32f%)?6Z`=$V7?E& z2yO>Qz;)m^c(Lptv^roNjYmB}p3WR4RK2FYZz>%OzVhDM#X-3?-v+AQcGZmQ6nj<+Vv zZAgP6&tLQzvj*$;?Z|8o1_hqu42JY*S60$!5Lt3aXuwDuqPWN-e&G4E1u01w@o!TY zc39LY5vq_rG!o{{vkbHx33_3Tpcm9gm-#g!47o3D z8llweS=UJH+EFNSIp}>cN*&CjLQ#c#-gYgo3yJM7uo~gt3pBDll;?R)WTTN09s&O3 zt$NdF*j%za9*v^!MQ^+3*2TbBa6)nWl2a<|Rg zv{d;Z+cYH041uv6r%=VnhL%P*)v8jkYFsIJbhGlr1YH`V>{i<*EKRkjIDS^e#pv2) z=B-xAty)XJkEPWu;(cf29K zU@Bx>^@ETk%jisux_$K0m=Sx0P-Rub4$+Gqh;5i)n#-pF?GTdOwS0k@QJ=WzZ1a39 zL7^vIxJ=q^t8o^AVo+j1WV5tGN!veMF+NMr6vNxd-IUb*ZF8C+cGRRbi61$$Zrsz+?vIbVUip^Ix$f*M4TKG zC*=eq3~z?|)U>0XO2Dlnjpu*M(HK}K$q|(pRTZva0@;-qqUBiLBngoNxm>2GaU<=j zYCJEGFZE@NomHMi3_t0}wZS4ME2MPuX&oDsRj}WzfI`>?xwtIlwim)S$WSrwNH(je0(gDs zbLD)Z?jgBydzBey_*zG{3;}AOm)!v?7#NkD&E{d}OFf^3bx-uqx*aa${g)e*ZHg`@ z5<$qsVC%jYM8sk%GmOEU5*XmERtwRz9(g6KY+Eu^bbF6Yt8o zCCN#lCOUY?7@?$VTa2vHz-Cd8cq_zqNs9EBt0({D;+!He*|WPBxslaeo*juucrgO5k-!rQS%dD^o_EK&@m8 zY0{uP+3eshAuF#Go@ba#SAQ+*%&vUj4`I(kv5N|$JS~*+RYCOzSGGq3iwo%IiH6yRYT1yuJfrL{ zGHJk9WQgMX|EIBUe;x4sKeoc+^F!?OFM&1iK`;j1LYjYoH`4Tf<9YN)E1(t73TOqi z0$KsBfL1^&pcT*xXa%$aSDgag=lUs2XtI0ilrZUu+HHt;9x{V#({ z;38N9^MLySoCdSt2Jjlr`d(eWCg8pSJHR$@4R{OZ|IdMk!9!pH zjDuUi&EQ7xI{X2i1?wOHcYyt1FW3Xdz`x-G@H_AV_$6?`BzO%kI(`M72akd}_#V#l zzYD$rz5>pH8^J$uW`7y{7(5J4f!*L)X!aBk+Ff8I_b7u(iwVN<)r^( zPpNY2#kNnhnRdJwhv#^RBCLiZc(OFK$uiGY{hj!pQkfzDuS}5t-@x9K#3Cy95eF?82_*a-rK7?xemI1D$aHizW5)r&b;rrF&{uQeXw*qfXbCnxMw@ou=EF!!v z+UVo(yHH+X@D$}vo+h4F<}=2 Date: Sun, 12 Mar 2017 17:00:33 +0000 Subject: [PATCH 14/53] Ignore swap files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 1bbc61e9..0dee7465 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ nginx.pid .idea /bin env +*.swp From 964cdd2b9a456dc80dc6bcd75e6c55cd9443d415 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 12 Mar 2017 19:11:40 +0000 Subject: [PATCH 15/53] Move boost in to pips component --- src/app/components/BattleCentre.jsx | 16 +-- src/app/components/Movement.jsx | 19 +--- src/app/components/Pips.jsx | 155 ++++++++++++++++------------ src/app/components/Shields.jsx | 4 +- src/less/movement.less | 13 --- src/less/pips.less | 67 +++++++----- 6 files changed, 144 insertions(+), 130 deletions(-) diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index addfe793..21a10c29 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -42,6 +42,7 @@ export default class BattleCentre extends TranslatedComponent { wep: 2, fuel: ship.fuelCapacity, cargo: ship.cargoCapacity, + boost: false, engagementRange: 1500, opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots) }; @@ -60,12 +61,13 @@ export default class BattleCentre extends TranslatedComponent { /** * Triggered when pips have been updated - * @param {number} sys SYS pips - * @param {number} eng ENG pips - * @param {number} wep WEP pips + * @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) { - this.setState({ sys, eng, wep }); + _pipsUpdated(sys, eng, wep, boost) { + this.setState({ sys, eng, wep, boost }); } /** @@ -108,7 +110,7 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { sys, eng, wep, cargo, fuel, engagementRange, opponent } = this.state; + const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent } = this.state; const { ship } = this.props; // Markers are used to propagate state changes @@ -131,7 +133,7 @@ export default class BattleCentre extends TranslatedComponent {
    - +
    ); diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index a098ea2e..62dbf544 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import cn from 'classnames'; import TranslatedComponent from './TranslatedComponent'; /** @@ -9,6 +8,7 @@ export default class Movement extends TranslatedComponent { static propTypes = { marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, + boost: React.PropTypes.bool.isRequired, eng: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, cargo: React.PropTypes.number.isRequired @@ -20,19 +20,6 @@ export default class Movement extends TranslatedComponent { */ constructor(props) { super(props); - - this._toggleBoost = this._toggleBoost.bind(this); - - this.state = { boost: false }; - } - - /** - * Toggle the boost feature - */ - _toggleBoost() { - let { boost } = this.state; - boost = !boost; - this.setState({ boost }); } /** @@ -40,10 +27,9 @@ export default class Movement extends TranslatedComponent { * @return {React.Component} contents */ render() { - const { ship, eng, cargo, fuel } = this.props; + const { ship, boost, eng, cargo, fuel } = this.props; const { language } = this.context; const { formats, translate, units } = language; - const { boost } = this.state; return ( @@ -79,7 +65,6 @@ export default class Movement extends TranslatedComponent { // Yaw {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s - { ship.canBoost() ? : null } ); } } diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index 8d123e82..185ddf74 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -11,7 +11,7 @@ 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) which is triggered whenever the pips change. + * Requires an onChange() function of the form onChange(sys, eng, wep, boost) which is triggered whenever the pips change. */ export default class Pips extends TranslatedComponent { static propTypes = { @@ -30,12 +30,14 @@ 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(), @@ -101,6 +103,10 @@ export default class Pips extends TranslatedComponent { */ _keyDown(e) { switch (e.keyCode) { + case 9: // Tab == boost + e.preventDefault(); + this._toggleBoost(); + break; case 37: // Left arrow == increase SYS e.preventDefault(); this._incSys(); @@ -120,15 +126,31 @@ export default class Pips extends TranslatedComponent { } } + /** + * Handle a click + * @param {string} which Which item was clicked + */ + onClick(which) { + if (which == 'SYS') { + this._incSys(); + } else if (which == 'ENG') { + this._incEng(); + } else if (which == 'WEP') { + this._incWep(); + } else if (which == 'RST') { + this._reset(); + } + } + /** * Reset the capacitor */ _reset() { - let { sys, eng, wep } = this.state; + let { sys, eng, wep, boost } = 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); + this.props.onChange(sys, eng, wep, boost); } } @@ -136,7 +158,7 @@ export default class Pips extends TranslatedComponent { * Increment the SYS capacitor */ _incSys() { - let { sys, eng, wep } = this.state; + let { sys, eng, wep, boost } = this.state; const required = Math.min(1, 4 - sys); if (required > 0) { @@ -164,7 +186,7 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep); + this.props.onChange(sys, eng, wep, boost); } } @@ -172,7 +194,7 @@ export default class Pips extends TranslatedComponent { * Increment the ENG capacitor */ _incEng() { - let { sys, eng, wep } = this.state; + let { sys, eng, wep, boost } = this.state; const required = Math.min(1, 4 - eng); if (required > 0) { @@ -200,7 +222,7 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep); + this.props.onChange(sys, eng, wep, boost); } } @@ -208,7 +230,7 @@ export default class Pips extends TranslatedComponent { * Increment the WEP capacitor */ _incWep() { - let { sys, eng, wep } = this.state; + let { sys, eng, wep, boost } = this.state; const required = Math.min(1, 4 - wep); if (required > 0) { @@ -236,24 +258,18 @@ export default class Pips extends TranslatedComponent { } } this.setState({ sys, eng, wep, pipsSvg: this._renderPips(sys, eng, wep) }); - this.props.onChange(sys, eng, wep); + this.props.onChange(sys, eng, wep, boost); } } /** - * Handle a click - * @param {string} which Which item was clicked + * Toggle the boost feature */ - onClick(which) { - if (which == 'SYS') { - this._incSys(); - } else if (which == 'ENG') { - this._incEng(); - } else if (which == 'WEP') { - this._incWep(); - } else if (which == 'RST') { - this._reset(); - } + _toggleBoost() { + let { boost, sys, eng, wep } = this.state; + boost = !boost; + this.setState({ boost }); + this.props.onChange(sys, eng, wep, boost); } /** @@ -269,37 +285,37 @@ export default class Pips extends TranslatedComponent { // SYS pipsSvg['SYS'] = []; for (let i = 0; i < Math.floor(sys); i++) { - pipsSvg['SYS'].push(); + pipsSvg['SYS'].push(); } if (sys > Math.floor(sys)) { - pipsSvg['SYS'].push(); + pipsSvg['SYS'].push(); } for (let i = Math.floor(sys + 0.5); i < 4; i++) { - pipsSvg['SYS'].push(); + pipsSvg['SYS'].push(); } // ENG pipsSvg['ENG'] = []; for (let i = 0; i < Math.floor(eng); i++) { - pipsSvg['ENG'].push(); + pipsSvg['ENG'].push(); } if (eng > Math.floor(eng)) { - pipsSvg['ENG'].push(); + pipsSvg['ENG'].push(); } for (let i = Math.floor(eng + 0.5); i < 4; i++) { - pipsSvg['ENG'].push(); + pipsSvg['ENG'].push(); } // WEP pipsSvg['WEP'] = []; for (let i = 0; i < Math.floor(wep); i++) { - pipsSvg['WEP'].push(); + pipsSvg['WEP'].push(); } if (wep > Math.floor(wep)) { - pipsSvg['WEP'].push(); + pipsSvg['WEP'].push(); } for (let i = Math.floor(wep + 0.5); i < 4; i++) { - pipsSvg['WEP'].push(); + pipsSvg['WEP'].push(); } return pipsSvg; @@ -312,7 +328,7 @@ 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 { boost, 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'); @@ -320,40 +336,49 @@ export default class Pips extends TranslatedComponent { const onRstClicked = this.onClick.bind(this, 'RST'); return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      {pipsSvg['ENG']} 
     {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
     {translate('SYS')}{translate('RST')}{translate('WEP')}
    {translate('capacity')} ({units.MJ}){formats.f1(sysCap)}{formats.f1(engCap)}{formats.f1(wepCap)}
    {translate('recharge')} ({units.MW}){formats.f1(sysRate * (sys / 4))}{formats.f1(engRate * (eng / 4))}{formats.f1(wepRate * (wep / 4))}
    +
    + + + { ship.canBoost() ? + + + + + + : null } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
       
      {pipsSvg['ENG']} 
     {pipsSvg['SYS']}{translate('ENG')}{pipsSvg['WEP']}
     {translate('SYS')}{translate('RST')}{translate('WEP')}
    {translate('capacity')} ({units.MJ}){formats.f1(sysCap)}{formats.f1(engCap)}{formats.f1(wepCap)}
    {translate('recharge')} ({units.MW}){formats.f1(sysRate * (sys / 4))}{formats.f1(engRate * (eng / 4))}{formats.f1(wepRate * (wep / 4))}
    +
    ); } } diff --git a/src/app/components/Shields.jsx b/src/app/components/Shields.jsx index 376f6f88..445d8e72 100644 --- a/src/app/components/Shields.jsx +++ b/src/app/components/Shields.jsx @@ -74,8 +74,8 @@ export default class Shields extends TranslatedComponent { // Remove base shield generator strength boost -= 1; boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2; - boosterKinDmg = boosterKinDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterKinDmg) / 2; - boosterThermDmg = boosterThermDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterThermDmg) / 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; diff --git a/src/less/movement.less b/src/less/movement.less index bfd43bac..724fae86 100644 --- a/src/less/movement.less +++ b/src/less/movement.less @@ -10,17 +10,4 @@ stroke: @primary; } } - - button { - font-size: 1.4em; - background: @primary-bg; - color: @primary; - border: 1px solid @primary; - &.boost { - // Shown when boost is enabled - background: @primary; - color: @primary-bg; - } - } - } diff --git a/src/less/pips.less b/src/less/pips.less index d1b09f43..10220914 100755 --- a/src/less/pips.less +++ b/src/less/pips.less @@ -1,29 +1,44 @@ // The pips table - keep the background black -.pipstable { - background-color: @bgBlack; - color: @primary; -} - -// A clickable entity in the pips table -.pipsclickable { - cursor: pointer; -} - -// A full pip -.fullpip { - stroke: @primary; - fill: @primary; -} - -// A half pip -.halfpip { - stroke: @primary-disabled; - fill: @primary-disabled; -} - -// An empty pip -.emptypip { - stroke: @primary-bg; - fill: @primary-bg; +#pips { + table { + background-color: @bgBlack; + color: @primary; + } + + // A clickable entity in the pips table + .clickable { + 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; + fill: @primary; + } + + // A half pip + .half { + stroke: @primary-disabled; + fill: @primary-disabled; + } + + // An empty pip + .empty { + stroke: @primary-bg; + fill: @primary-bg; + } } From 0729fc29fa5164a4e621ad46409e9a72bbee4f05 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 13 Mar 2017 17:07:39 +0000 Subject: [PATCH 16/53] Add vertical bar chart and use it in battle centre --- package.json | 1 + src/app/components/BattleCentre.jsx | 27 +- src/app/components/Defence.jsx | 421 ++++++++++++++++++++++++ src/app/components/EngagementRange.jsx | 10 +- src/app/components/PieChart.jsx | 78 +++++ src/app/components/Pips.jsx | 44 +-- src/app/components/Shields.jsx | 194 ----------- src/app/components/VerticalBarChart.jsx | 122 +++++++ src/app/shipyard/Ship.js | 6 +- src/less/app.less | 1 + src/less/defence.less | 14 + src/less/outfit.less | 8 + src/less/pips.less | 2 + 13 files changed, 699 insertions(+), 229 deletions(-) create mode 100644 src/app/components/Defence.jsx create mode 100644 src/app/components/PieChart.jsx delete mode 100644 src/app/components/Shields.jsx create mode 100644 src/app/components/VerticalBarChart.jsx create mode 100755 src/less/defence.less diff --git a/package.json b/package.json index 6f9d4a16..113b5fc9 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "less": "^2.5.3", "less-loader": "^2.2.1", "react-addons-test-utils": "^15.0.1", + "@types/react-measure": "^0.4.6", "react-testutils-additions": "^15.1.0", "rimraf": "^2.4.3", "rollup": "0.36", diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index 21a10c29..d0f19138 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -9,7 +9,7 @@ import Cargo from './Cargo'; import Movement from './Movement'; import EngagementRange from './EngagementRange'; import ShipPicker from './ShipPicker'; -import Shields from './Shields'; +import Defence from './Defence'; /** * Battle centre allows you to pit your current build against another ship, @@ -113,28 +113,31 @@ export default class BattleCentre extends TranslatedComponent { const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent } = this.state; const { ship } = this.props; - // Markers are used to propagate state changes - const movementMarker = '' + ship.topSpeed + ':' + ship.pitch + ':' + ship.roll + ':' + ship.yaw; - const shieldMarker = '' + ship.shield + ':' + ship.cells + ':' + ship.shieldExplRes + ':' + ship.shieldKinRes + ':' + ship.shieldThermRes; + // Markers are used to propagate state changes without requiring a deep comparison of the ship + 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; return (

    {translate('battle centre')}

    -
    - -
    -
    +

    {translate('ship management')}

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

    {translate('opponent')}

    +
    - -
    -
    +

    {translate('movement')}

    +
    +

    {translate('defence')}

    + +
    ); } diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx new file mode 100644 index 00000000..03c3a3be --- /dev/null +++ b/src/app/components/Defence.jsx @@ -0,0 +1,421 @@ +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'; + +/** + * Defence information + * Shield information consists of four panels: + * - textual information (time to lose shields etc.) + * - breakdown of shield sources (pie chart) + * - comparison of shield resistances (bar chart) + * - effective shield (bar chart) + */ +export default class Defence extends TranslatedComponent { + static propTypes = { + marker: React.PropTypes.string.isRequired, + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + sys: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + const { shield, armour, damagetaken } = this._calcMetrics(props.ship, props.opponent, props.sys); + this.state = { shield, armour, damagetaken }; + } + + /** + * 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, damagetaken } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); + this.setState({ shield, armour, damagetaken }); + return true; + } + } + + /** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The opponent ship + * @returns {Object} Shield metrics + */ + _calcMetrics(ship, opponent, sys) { + const sysResistance = this._calcSysResistance(sys); + + 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; + shield = { + generator: generatorStrength, + boosters: boostersStrength, + cells: ship.shieldCells, + total: generatorStrength + boostersStrength + ship.shieldCells + }; + + // 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 + }; + + shield.explosive = { + generator: 1 - shieldGenerator.getExplosiveResistance(), + boosters: boosterExplDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance) + }; + + shield.kinetic = { + generator: 1 - shieldGenerator.getKineticResistance(), + boosters: boosterKinDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance) + }; + + shield.thermal = { + generator: 1 - shieldGenerator.getThermalResistance(), + boosters: boosterThermDmg, + sys: (1 - sysResistance), + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance) + }; + } + + // 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 + }; + + // Use the SDPS for each weapon type of the opponent to work out how long the shields and armour will last + // const opponentSDps = Calc.sustainedDps(opponent, range); + const opponentSDps = { + absolute: 62.1, + explosive: 0, + kinetic: 7.4, + thermal: 7.4 + }; + + // Modify according to resistances to see how much damage we actually take + //opponentSDps.absolute *= shield.absolute.total; + //opponentSDps.explosive *= shield.explosive.total; + //opponentSDps.kinetic *= shield.kinetic.total; + //opponentSDps.thermal *= shield.thermal.total; + opponentSDps.total = opponentSDps.absolute + opponentSDps.explosive + opponentSDps.kinetic + opponentSDps.thermal; + + const damagetaken = { + absolutesdps: opponentSDps.absolute, + explosivesdps: opponentSDps.explosive, + kineticsdps: opponentSDps.kinetic, + thermalsdps: opponentSDps.thermal, + tts: (shield.total + ship.shieldCells) / opponentSDps.total, + }; + + return { shield, armour, damagetaken }; + } + + /** + * 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, damagetaken } = this.state; + + const shieldSourcesData = []; + const effectiveShieldData = []; + const damageTakenData = []; + const shieldTooltipDetails = []; + const shieldAbsoluteTooltipDetails = []; + const shieldExplosiveTooltipDetails = []; + const shieldKineticTooltipDetails = []; + const shieldThermalTooltipDetails = []; + let effectiveAbsoluteShield = 0; + let effectiveExplosiveShield = 0; + let effectiveKineticShield = 0; + let effectiveThermalShield = 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)}
    ); + + effectiveAbsoluteShield = shield.total / shield.absolute.total; + effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') }); + effectiveExplosiveShield = shield.total / shield.explosive.total; + effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') }); + effectiveKineticShield = shield.total / shield.kinetic.total; + effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') }); + effectiveThermalShield = shield.total / shield.thermal.total; + effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') }); + + damageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute') }); + damageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive') }); + damageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic') }); + damageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal') }); + } + + const armourData = []; + if (Math.round(armour.bulkheads) > 0) armourData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); + if (Math.round(armour.reinforcement) > 0) armourData.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)}
    ); + + return ( + + {shield.total ? +
    +
    +

    {shieldTooltipDetails}

    )} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ} + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    {translate('damage type')} +   + +   + +   + +   +
    {translate('damage taken')} + {shieldAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.absolute.total)} + + {shieldExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.explosive.total)} + + {shieldKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.kinetic.total)} + + {shieldThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.thermal.total)} +
    {translate('effective shield')} + {formats.int(effectiveAbsoluteShield)}{units.MJ} + + {formats.int(effectiveExplosiveShield)}{units.MJ} + + {formats.int(effectiveKineticShield)}{units.MJ} + + {formats.int(effectiveThermalShield)}{units.MJ} +
    {translate('shields will hold against opponent for')} {formats.time(damagetaken.tts)}
    +
    +
    +

    {translate('shield sources')}

    + +
    + +
    +
    +

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

    + +
    +
    +

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

    + +
    +
    : null } + +
    +

    {armourTooltipDetails}

    )} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('armour')}: {formats.int(armour.total)} + + + + + + + + + + +
    {translate('damage taken')} + {armourAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.absolute.total)} + + {armourExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.explosive.total)} + + {armourKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.kinetic.total)} + + {armourThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.thermal.total)} +
    + +
    +

    {translate('armour sources')}

    + +
    +
    ); + } +} diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index 2f624bd0..ef851b01 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -27,10 +27,18 @@ export default class Range extends TranslatedComponent { this.state = { maxRange, - rangeLevel: 1, + rangeLevel: 0.5, }; } + /** + * + */ + 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 diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx new file mode 100644 index 00000000..c9199641 --- /dev/null +++ b/src/app/components/PieChart.jsx @@ -0,0 +1,78 @@ +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import * as d3 from 'd3'; + +const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ]; +const LABEL_COLOUR = '#FFFFFF'; + +/** + * A pie chart + */ +export default class PieChart extends Component { + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.pie = d3.pie().value((d) => d.value); + this.colors = CORIOLIS_COLOURS; + this.arc = d3.arc(); + this.arc.innerRadius(0); + + this.state = { + dimensions: { + width: 100, + height: 100 + } + } + } + + + /** + * Generate a slice of the pie chart + */ + sliceGenerator(d, i) { + const { width, height } = this.state.dimensions; + const { data } = this.props; + + // Push the labels further out from the centre of the slice + let [labelX, labelY] = this.arc.centroid(d); + const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; + + // Put the keys in a line with equal spacing + const keyX = -width / 2 + (width / data.length) * (i + 0.5); + const keyTranslate = `translate(${keyX}, ${width * 0.45})`; + + return ( + + + {d.value} + {d.data.label} + + ); + } + + render() { + const { width, height } = this.state.dimensions; + const pie = this.pie(this.props.data), + translate = `translate(${width / 2}, ${width * 0.4})`; + + this.arc.outerRadius(width * 0.4); + + return ( + { this.setState({dimensions}) }}> +
    + + + {pie.map((d, i) => this.sliceGenerator(d, i))} + + +
    +
    + ); + } +} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index 185ddf74..d5446594 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -15,6 +15,7 @@ import Module from '../shipyard/Module'; */ export default class Pips extends TranslatedComponent { static propTypes = { + marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, onChange: React.PropTypes.func.isRequired }; @@ -68,9 +69,9 @@ 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 } = this.state; - const ship = nextProps.ship; - const pd = ship.standard[4].m; + const { sysCap, engCap, wepCap, sysRate, engRate, wepRate, boost } = this.state; + const nextShip = nextProps.ship; + const pd = nextShip.standard[4].m; const nextSysCap = pd.getSystemsCapacity(); const nextEngCap = pd.getEnginesCapacity(); @@ -78,19 +79,22 @@ 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) { + nextWepRate != wepRate || + nextBoost != boost) { this.setState({ sysCap: nextSysCap, engCap: nextEngCap, wepCap: nextWepCap, sysRate: nextSysRate, engRate: nextEngRate, - wepRate: nextWepRate + wepRate: nextWepRate, + boost: nextBoost }); } @@ -104,8 +108,10 @@ export default class Pips extends TranslatedComponent { _keyDown(e) { switch (e.keyCode) { case 9: // Tab == boost - e.preventDefault(); - this._toggleBoost(); + if (this.props.ship.canBoost()) { + e.preventDefault(); + this._toggleBoost(); + } break; case 37: // Left arrow == increase SYS e.preventDefault(); @@ -364,21 +370,21 @@ export default class Pips extends TranslatedComponent { {translate('RST')} {translate('WEP')} - - {translate('capacity')} ({units.MJ}) - {formats.f1(sysCap)} - {formats.f1(engCap)} - {formats.f1(wepCap)} - - - {translate('recharge')} ({units.MW}) - {formats.f1(sysRate * (sys / 4))} - {formats.f1(engRate * (eng / 4))} - {formats.f1(wepRate * (wep / 4))} - ); } } +// +// {translate('capacity')} ({units.MJ}) +// {formats.f1(sysCap)} +// {formats.f1(engCap)} +// {formats.f1(wepCap)} +// +// +// {translate('recharge')} ({units.MW}) +// {formats.f1(sysRate * (sys / 4))} +// {formats.f1(engRate * (eng / 4))} +// {formats.f1(wepRate * (wep / 4))} +// diff --git a/src/app/components/Shields.jsx b/src/app/components/Shields.jsx deleted file mode 100644 index 445d8e72..00000000 --- a/src/app/components/Shields.jsx +++ /dev/null @@ -1,194 +0,0 @@ -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'; - -/** - * Shields - * Effective shield strength (including SCBs) - * Time for opponent to down shields - * - need sustained DPS for each type of damage (K/T/E/R) - * - turn in to % of shields removed per second - */ -export default class Shields extends TranslatedComponent { - static propTypes = { - marker: React.PropTypes.string.isRequired, - ship: React.PropTypes.object.isRequired, - opponent: React.PropTypes.object.isRequired, - sys: React.PropTypes.number.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - - const { shield, absolute, explosive, kinetic, thermal } = this._calcMetrics(props.ship, props.opponent, props.sys); - this.state = { shield, absolute, explosive, kinetic, thermal }; - } - - /** - * 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, absolute, explosive, kinetic, thermal } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); - this.setState({ shield, absolute, explosive, kinetic, thermal }); - return true; - } - } - - /** - * Calculate shield metrics - * @param {Object} ship The ship - * @param {Object} opponent The opponent ship - * @param {int} sys The opponent ship - * @returns {Object} Shield metrics - */ - _calcMetrics(ship, opponent, sys) { - const sysResistance = this._calcSysResistance(sys); - - const shieldGenerator = ship.findShieldGenerator(); - - // 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; - 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; - const shield = { - generator: generatorStrength, - boosters: boostersStrength, - total: generatorStrength + boostersStrength - }; - - // Resistances have three components: the shield generator, the shield boosters and the SYS pips. - // We re-cast these as damage percentages - const absolute = { - generator: 1, - boosters: 1, - sys: 1 - sysResistance, - total: 1 - sysResistance - }; - - const explosive = { - generator: 1 - shieldGenerator.getExplosiveResistance(), - boosters: boosterExplDmg, - sys: (1 - sysResistance), - total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance) - }; - - const kinetic = { - generator: 1 - shieldGenerator.getKineticResistance(), - boosters: boosterKinDmg, - sys: (1 - sysResistance), - total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance) - }; - - const thermal = { - generator: 1 - shieldGenerator.getThermalResistance(), - boosters: boosterThermDmg, - sys: (1 - sysResistance), - total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance) - }; - - return { shield, absolute, explosive, kinetic, thermal }; - } - - /** - * 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, absolute, explosive, kinetic, thermal } = this.state; - - const shieldTooltipDetails = []; - shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); - shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); - - const absoluteTooltipDetails = []; - absoluteTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(absolute.generator)}
    ); - absoluteTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(absolute.boosters)}
    ); - absoluteTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(absolute.sys)}
    ); - - const explosiveTooltipDetails = []; - explosiveTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(explosive.generator)}
    ); - explosiveTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(explosive.boosters)}
    ); - explosiveTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(explosive.sys)}
    ); - - const kineticTooltipDetails = []; - kineticTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(kinetic.generator)}
    ); - kineticTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(kinetic.boosters)}
    ); - kineticTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(kinetic.sys)}
    ); - - const thermalTooltipDetails = []; - thermalTooltipDetails.push(
    {translate('generator') + ' ' + formats.pct1(thermal.generator)}
    ); - thermalTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(thermal.boosters)}
    ); - thermalTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(thermal.sys)}
    ); - - return ( - - - - - - - - - - - - - - -
    {shieldTooltipDetails})} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ}
    {translate('damage from')} -   - {absoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(absolute.total)} - -   - {explosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(explosive.total)} - -   - {kineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(kinetic.total)} - -   - {thermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(thermal.total)} -
    -
    ); - } -} diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx new file mode 100644 index 00000000..1eab7614 --- /dev/null +++ b/src/app/components/VerticalBarChart.jsx @@ -0,0 +1,122 @@ +import React, { Component } from 'react'; +import Measure from 'react-measure'; +import * as d3 from 'd3'; + +const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ]; +const LABEL_COLOUR = '#FFFFFF'; + +var margin = {top: 10, right: 0, bottom: 0, left: 50}; + +const ASPECT = 1; + +const merge = function(one, two) { + return Object.assign({}, one, two); +}; + +/** + * A vertical bar chart + */ +export default class VerticalBarChart extends Component { + + static propTypes = { + data : React.PropTypes.array.isRequired, + ylabel : React.PropTypes.string + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.state = { + dimensions: { + width: 300, + height: 300 + } + } + } + + _renderGraph(props){ + let { width, height } = this.state.dimensions; + + width = width - margin.left - margin.right, + height = width * ASPECT - margin.top - margin.bottom; + + // X axis is a band scale with values being 'label' + this.x = d3.scaleBand(); + this.x.domain(this.props.data.map(d => d.label)).padding(0.2); + this.xAxis = d3.axisBottom(this.x).tickValues(this.props.data.map(d => d.label)); + this.x.range([0, width]); + + // Y axis is a numeric scale with values being 'value' + this.y = d3.scaleLinear(); + this.y.domain([0, d3.max(this.props.data, d => d.value)]); + this.yAxis = d3.axisLeft(this.y); + this.y.range([height, 0]); + + let svg = d3.select(this.svg).select('g'); + + svg.selectAll('rect').remove(); + svg.selectAll('text').remove(); + + svg.select('.x.axis').remove(); + svg.select('.y.axis').remove(); + + svg.append('g') + .attr('class', 'x axis') + .attr('transform', `translate(0, ${height})`) + .call(this.xAxis); + + svg.append('g') + .attr('class', 'y axis') + .call(this.yAxis) + .attr('fill', CORIOLIS_COLOURS[0]) + + svg.selectAll('rect.bar') + .data(props.data) + .enter().append('rect') + .attr('class', 'bar') + .attr('x', d => this.x(d.label)) + .attr('width', this.x.bandwidth()) + .attr('y', d => this.y(d.value)) + .attr('height', d => height - this.y(d.value)) + .attr('fill', CORIOLIS_COLOURS[0]); + + svg.selectAll('text.bar') + .data(props.data) + .enter().append('text') + .attr('class', 'bar') + .attr('text-anchor', 'middle') + .attr('x', 100) + .attr('y', 100) + .attr('stroke', '#ffffff') + .attr('stroke-width', '1px') + .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) + .attr('y', d => this.y(d.value) + 15) + .text(d => d.value); + } + + + + render() { + const { width } = this.state.dimensions; + + const translate = `translate(${margin.left}, ${margin.top})`; + + this._renderGraph(this.props); + + return ( + { this.setState({dimensions}) }}> +
    + { this.x ? + this.svg = ref} width={width} height={width * ASPECT} transform={translate}> + + : null } +
    +
    + ); + } +} diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 10da4582..785c8d27 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1348,13 +1348,13 @@ export default class Ship { let moduleprotection = 1; let hullExplRes = 1 - bulkhead.getExplosiveResistance(); const hullExplResDRStart = hullExplRes * 0.7; - const hullExplResDREnd = hullExplRes * 0; // Currently don't know where this is + const hullExplResDREnd = hullExplRes * 0; let hullKinRes = 1 - bulkhead.getKineticResistance(); const hullKinResDRStart = hullKinRes * 0.7; - const hullKinResDREnd = hullKinRes * 0; // Currently don't know where this is + const hullKinResDREnd = hullKinRes * 0; let hullThermRes = 1 - bulkhead.getThermalResistance(); const hullThermResDRStart = hullThermRes * 0.7; - const hullThermResDREnd = hullThermRes * 0; // Currently don't know where this is + const hullThermResDREnd = hullThermRes * 0; // Armour from HRPs and module armour from MRPs for (let slot of this.internal) { diff --git a/src/less/app.less b/src/less/app.less index eb14b618..3dc03d2d 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -22,6 +22,7 @@ @import 'pips'; @import 'movement'; @import 'shippicker'; +@import 'defence'; html, body { height: 100%; diff --git a/src/less/defence.less b/src/less/defence.less new file mode 100755 index 00000000..2c5c730e --- /dev/null +++ b/src/less/defence.less @@ -0,0 +1,14 @@ +#defence { + table { + background-color: @bgBlack; + color: @primary; + margin: 0 auto; + } + + .icon { + stroke: @primary; + stroke-width: 20; + fill: transparent; + } +} + diff --git a/src/less/outfit.less b/src/less/outfit.less index 7b792542..b97c354c 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -206,6 +206,14 @@ }); } + &.full { + width: 100%; + + .smallTablet({ + width: 100% !important; + }); + } + .smallScreen({ .axis.x { g.tick:nth-child(2n + 1) text { diff --git a/src/less/pips.less b/src/less/pips.less index 10220914..1db973ae 100755 --- a/src/less/pips.less +++ b/src/less/pips.less @@ -1,8 +1,10 @@ // The pips table - keep the background black #pips { + table { background-color: @bgBlack; color: @primary; + margin: 0 auto; } // A clickable entity in the pips table From 1605e80884f7c24a66aa16a3b3eadbcbe011e2af Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 13 Mar 2017 19:55:45 +0000 Subject: [PATCH 17/53] Fix to calculate boost when engcap changes --- ChangeLog.md | 1 + src/app/components/VerticalBarChart.jsx | 2 +- src/app/shipyard/Ship.js | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 54920061..eff8db7f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,7 @@ * Fix issue where new module added to a slot did not reset its enabled status * Show integrity value for relevant modules * Reset old modification values when a new roll is applied + * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 1eab7614..45183cf5 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -5,7 +5,7 @@ import * as d3 from 'd3'; const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ]; const LABEL_COLOUR = '#FFFFFF'; -var margin = {top: 10, right: 0, bottom: 0, left: 50}; +var margin = {top: 10, right: 0, bottom: 0, left: 55}; const ASPECT = 1; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 785c8d27..8bb0b84b 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -553,6 +553,10 @@ export default class Ship { } else if (name === 'wepcap' || name === 'weprate') { m.setModValue(name, value, sentfromui); this.recalculateTtd(); + } else if (name === 'engcap') { + m.setModValue(name, value, sentfromui); + // Might have resulted in a change in boostability + this.updateMovement(); } else { // Generic m.setModValue(name, value, sentfromui); @@ -1247,6 +1251,7 @@ export default class Ship { * @return {this} The ship instance (for chaining operations) */ updateMovement() { +console.log('updateMovement()'); this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed); this.topSpeed = this.speeds[4]; this.topBoost = this.canBoost() ? this.speeds[4] * this.boost / this.speed : 0; From 7b3ad555a17cd9171d0ada96ee7bcc4b79fdc0af Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 13 Mar 2017 22:13:41 +0000 Subject: [PATCH 18/53] Tidy-ups --- src/app/components/Defence.jsx | 107 ++++++------------------ src/app/components/PieChart.jsx | 21 +++-- src/app/components/Pips.jsx | 4 +- src/app/components/VerticalBarChart.jsx | 22 +++-- src/app/i18n/en.js | 16 ++++ src/app/shipyard/Ship.js | 1 - 6 files changed, 70 insertions(+), 101 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 03c3a3be..7c9c0dfe 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -29,8 +29,8 @@ export default class Defence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, damagetaken } = this._calcMetrics(props.ship, props.opponent, props.sys); - this.state = { shield, armour, damagetaken }; + const { shield, armour, shielddamage } = this._calcMetrics(props.ship, props.opponent, props.sys); + this.state = { shield, armour, shielddamage }; } /** @@ -40,8 +40,8 @@ export default class Defence extends TranslatedComponent { */ componentWillReceiveProps(nextProps) { if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { - const { shield, armour, damagetaken } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); - this.setState({ shield, armour, damagetaken }); + const { shield, armour, shielddamage } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); + this.setState({ shield, armour, shielddamage }); return true; } } @@ -56,6 +56,16 @@ export default class Defence extends TranslatedComponent { _calcMetrics(ship, opponent, sys) { const sysResistance = this._calcSysResistance(sys); + // Obtain the opponent's sustained DPS for later damage calculations + // const opponentSDps = Calc.sustainedDps(opponent, range); + const opponentSDps = { + absolute: 62.1, + explosive: 0, + kinetic: 7.4, + thermal: 7.4 + }; + + let shielddamage = {}; let shield = {}; const shieldGeneratorSlot = ship.findInternalByGroup('sg'); if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { @@ -122,11 +132,17 @@ export default class Defence extends TranslatedComponent { sys: (1 - sysResistance), total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance) }; + + shielddamage.absolutesdps = opponentSDps.absolute *= shield.absolute.total; + shielddamage.explosivesdps = opponentSDps.explosive *= shield.explosive.total; + shielddamage.kineticsdps = opponentSDps.kinetic *= shield.kinetic.total; + shielddamage.thermalsdps = opponentSDps.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 armourReinforcement = 0; let modulearmour = 0; let moduleprotection = 1; @@ -193,31 +209,7 @@ export default class Defence extends TranslatedComponent { total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg }; - // Use the SDPS for each weapon type of the opponent to work out how long the shields and armour will last - // const opponentSDps = Calc.sustainedDps(opponent, range); - const opponentSDps = { - absolute: 62.1, - explosive: 0, - kinetic: 7.4, - thermal: 7.4 - }; - - // Modify according to resistances to see how much damage we actually take - //opponentSDps.absolute *= shield.absolute.total; - //opponentSDps.explosive *= shield.explosive.total; - //opponentSDps.kinetic *= shield.kinetic.total; - //opponentSDps.thermal *= shield.thermal.total; - opponentSDps.total = opponentSDps.absolute + opponentSDps.explosive + opponentSDps.kinetic + opponentSDps.thermal; - - const damagetaken = { - absolutesdps: opponentSDps.absolute, - explosivesdps: opponentSDps.explosive, - kineticsdps: opponentSDps.kinetic, - thermalsdps: opponentSDps.thermal, - tts: (shield.total + ship.shieldCells) / opponentSDps.total, - }; - - return { shield, armour, damagetaken }; + return { shield, armour, shielddamage }; } /** @@ -237,7 +229,7 @@ export default class Defence extends TranslatedComponent { const { ship, sys } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { shield, armour, damagetaken } = this.state; + const { shield, armour, shielddamage } = this.state; const shieldSourcesData = []; const effectiveShieldData = []; @@ -321,58 +313,7 @@ export default class Defence extends TranslatedComponent {

    {shieldTooltipDetails}

    )} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ} - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {translate('damage type')} -   - -   - -   - -   -
    {translate('damage taken')} - {shieldAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.absolute.total)} - - {shieldExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.explosive.total)} - - {shieldKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.kinetic.total)} - - {shieldThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.thermal.total)} -
    {translate('effective shield')} - {formats.int(effectiveAbsoluteShield)}{units.MJ} - - {formats.int(effectiveExplosiveShield)}{units.MJ} - - {formats.int(effectiveKineticShield)}{units.MJ} - - {formats.int(effectiveThermalShield)}{units.MJ} -
    {translate('shields will hold against opponent for')} {formats.time(damagetaken.tts)}
    +

    {translate('PHRASE_TIME_TO_LOSE_SHIELDS')} {formats.time(shield.total / shielddamage.totalsdps)}

    {translate('shield sources')}

    diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index c9199641..0af72456 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import Measure from 'react-measure'; import * as d3 from 'd3'; -const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ]; +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#519032', '#D5420D']; const LABEL_COLOUR = '#FFFFFF'; /** @@ -28,12 +28,15 @@ export default class PieChart extends Component { width: 100, height: 100 } - } + }; } /** * Generate a slice of the pie chart + * @param {Object} d the data for this slice + * @param {number} i the index of this slice + * @returns {Object} the SVG for the slice */ sliceGenerator(d, i) { const { width, height } = this.state.dimensions; @@ -49,24 +52,28 @@ export default class PieChart extends Component { return ( - + {d.value} - {d.data.label} + {d.data.label} ); } + /** + * Render the component + * @returns {object} Markup + */ render() { const { width, height } = this.state.dimensions; const pie = this.pie(this.props.data), - translate = `translate(${width / 2}, ${width * 0.4})`; + translate = `translate(${width / 2}, ${width * 0.4})`; this.arc.outerRadius(width * 0.4); return ( - { this.setState({dimensions}) }}> + { this.setState({ dimensions }); }}>
    - + {pie.map((d, i) => this.sliceGenerator(d, i))} diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index d5446594..2bec028e 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -111,7 +111,7 @@ export default class Pips extends TranslatedComponent { if (this.props.ship.canBoost()) { e.preventDefault(); this._toggleBoost(); - } + } break; case 37: // Left arrow == increase SYS e.preventDefault(); @@ -345,7 +345,7 @@ export default class Pips extends TranslatedComponent {
    - { ship.canBoost() ? + { ship.canBoost() ? diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 45183cf5..9bbab161 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -2,10 +2,10 @@ import React, { Component } from 'react'; import Measure from 'react-measure'; import * as d3 from 'd3'; -const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ]; +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#519032', '#D5420D']; const LABEL_COLOUR = '#FFFFFF'; -var margin = {top: 10, right: 0, bottom: 0, left: 55}; +const margin = { top: 10, right: 0, bottom: 0, left: 55 }; const ASPECT = 1; @@ -36,10 +36,14 @@ export default class VerticalBarChart extends Component { width: 300, height: 300 } - } + }; } - _renderGraph(props){ + /** + * Render the graph + * @param {Object} props React Component properties + */ + _renderGraph(props) { let { width, height } = this.state.dimensions; width = width - margin.left - margin.right, @@ -73,7 +77,7 @@ export default class VerticalBarChart extends Component { svg.append('g') .attr('class', 'y axis') .call(this.yAxis) - .attr('fill', CORIOLIS_COLOURS[0]) + .attr('fill', CORIOLIS_COLOURS[0]); svg.selectAll('rect.bar') .data(props.data) @@ -99,8 +103,10 @@ export default class VerticalBarChart extends Component { .text(d => d.value); } - - + /** + * Render the component + * @returns {object} Markup + */ render() { const { width } = this.state.dimensions; @@ -109,7 +115,7 @@ export default class VerticalBarChart extends Component { this._renderGraph(this.props); return ( - { this.setState({dimensions}) }}> + { this.setState({ dimensions }); }}>
    { this.x ? this.svg = ref} width={width} height={width * ASPECT} transform={translate}> diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 4b338ae4..7cce1ea2 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -39,6 +39,7 @@ export const terms = { PHRASE_NO_SPECIAL: 'No experimental effect', PHRASE_SHOPPING_LIST: 'Stations that sell this build', PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells', + PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', @@ -204,6 +205,21 @@ export const terms = { optmul_sg: 'Optimal strength', maxmul_sg: 'Minimum strength', + // Damage types + absolute: 'Absolute', + explosive: 'Explosive', + kinetic: 'Kinetia', + thermal: 'Thermal', + + // Shield sources + generator: 'Generator', + boosters: 'Boosters', + cells: 'Cells', + + // Armour sources + bulkheads: 'Bulkheads', + reinforcement: 'Reinforcement', + // Help text HELP_TEXT: `

    Introduction

    diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 8bb0b84b..9a591fdc 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1251,7 +1251,6 @@ export default class Ship { * @return {this} The ship instance (for chaining operations) */ updateMovement() { -console.log('updateMovement()'); this.speeds = Calc.speed(this.unladenMass + this.fuelCapacity, this.speed, this.standard[1].m, this.pipSpeed); this.topSpeed = this.speeds[4]; this.topBoost = this.canBoost() ? this.speeds[4] * this.boost / this.speed : 0; From a3c03266bfd906916ad37a16359c027f4c9696d7 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 13 Mar 2017 22:59:01 +0000 Subject: [PATCH 19/53] On-going tidy-ups --- src/app/components/Defence.jsx | 123 ++++++++++++------------ src/app/components/VerticalBarChart.jsx | 10 +- src/app/i18n/en.js | 11 ++- src/less/outfit.less | 16 ++- 4 files changed, 96 insertions(+), 64 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 7c9c0dfe..cb9bdf82 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -233,16 +233,12 @@ export default class Defence extends TranslatedComponent { const shieldSourcesData = []; const effectiveShieldData = []; - const damageTakenData = []; + const shieldDamageTakenData = []; const shieldTooltipDetails = []; const shieldAbsoluteTooltipDetails = []; const shieldExplosiveTooltipDetails = []; const shieldKineticTooltipDetails = []; const shieldThermalTooltipDetails = []; - let effectiveAbsoluteShield = 0; - let effectiveExplosiveShield = 0; - let effectiveKineticShield = 0; - let effectiveThermalShield = 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') }); @@ -268,24 +264,24 @@ export default class Defence extends TranslatedComponent { shieldThermalTooltipDetails.push(
    {translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}
    ); shieldThermalTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}
    ); - effectiveAbsoluteShield = shield.total / shield.absolute.total; + const effectiveAbsoluteShield = shield.total / shield.absolute.total; effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') }); - effectiveExplosiveShield = shield.total / shield.explosive.total; + const effectiveExplosiveShield = shield.total / shield.explosive.total; effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') }); - effectiveKineticShield = shield.total / shield.kinetic.total; + const effectiveKineticShield = shield.total / shield.kinetic.total; effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') }); - effectiveThermalShield = shield.total / shield.thermal.total; + const effectiveThermalShield = shield.total / shield.thermal.total; effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') }); - damageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute') }); - damageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive') }); - damageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic') }); - damageTakenData.push({ value: Math.round(shield.thermal.total * 100), 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') }); } - const armourData = []; - if (Math.round(armour.bulkheads) > 0) armourData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); - if (Math.round(armour.reinforcement) > 0) armourData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); + 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)}
    ); @@ -307,55 +303,64 @@ export default class Defence extends TranslatedComponent { 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') }); + + // TODO these aren't updating when HRP metrics are altered (maybe - need to confirm) + 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') }); + + // TODO versions of ship.calcShieldRecovery() and ship.calcShieldRecharge() that take account of SYS pips return ( {shield.total ? -
    -
    -

    {shieldTooltipDetails}

    )} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ} -

    {translate('PHRASE_TIME_TO_LOSE_SHIELDS')} {formats.time(shield.total / shielddamage.totalsdps)}

    -
    -
    -

    {translate('shield sources')}

    - -
    +
    +

    {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')} {formats.time(shield.total / shielddamage.totalsdps)}

    +

    {translate('PHRASE_TIME_TO_RECOVER_SHIELDS')} {formats.time(ship.calcShieldRecovery())}

    +

    {translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')} {formats.time(ship.calcShieldRecharge())}

    -
    -
    -

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

    - -
    -
    -

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

    - -
    -
    : null } +
    +

    {translate('shield sources')}

    + +
    +
    +

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

    + +
    +
    +

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

    + +
    + : null } -
    -

    {armourTooltipDetails}

    )} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('armour')}: {formats.int(armour.total)} -
       
    - - - - - - - - - -
    {translate('damage taken')} - {armourAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.absolute.total)} - - {armourExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.explosive.total)} - - {armourKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.kinetic.total)} - - {armourThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.thermal.total)} -
    +
    +

    {translate('armour metrics')}

    +
    -
    -

    {translate('armour sources')}

    - +
    +

    {translate('armour sources')}

    + +
    +
    +

    {translate('effective armour')}

    + +
    +
    +

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

    +
    ); } diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 9bbab161..34166491 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -20,7 +20,7 @@ export default class VerticalBarChart extends Component { static propTypes = { data : React.PropTypes.array.isRequired, - ylabel : React.PropTypes.string + yMax : React.PropTypes.number }; /** @@ -57,7 +57,13 @@ export default class VerticalBarChart extends Component { // Y axis is a numeric scale with values being 'value' this.y = d3.scaleLinear(); - this.y.domain([0, d3.max(this.props.data, d => d.value)]); + if (props.yMax) { + // Fixed maximum value (unless we go off the scale) + const localMax = d3.max(this.props.data, d => d.value); + this.y.domain([0, localMax > props.yMax ? localMax : props.yMax]); + } else { + this.y.domain([0, d3.max(this.props.data, d => d.value)]); + } this.yAxis = d3.axisLeft(this.y); this.y.range([height, 0]); diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 7cce1ea2..74e2a3b2 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -25,7 +25,7 @@ export const terms = { PHRASE_NO_RETROCH: 'No Retrofitting changes', PHRASE_SELECT_BUILDS: 'Select builds to compare', PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge', - PHRASE_SG_RECOVER: 'Recovery (to 50%) after collapse', + PHRASE_SG_RECOVER: 'Time from 0% to 50% charge', PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target', @@ -40,6 +40,13 @@ export const terms = { PHRASE_SHOPPING_LIST: 'Stations that sell this build', PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells', PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for', + PHRASE_TIME_TO_RECOVER_SHIELDS: 'Shields will recover in', + PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in', + PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy', + PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types', + PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types', + + TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', @@ -208,7 +215,7 @@ export const terms = { // Damage types absolute: 'Absolute', explosive: 'Explosive', - kinetic: 'Kinetia', + kinetic: 'Kinetic', thermal: 'Thermal', // Shield sources diff --git a/src/less/outfit.less b/src/less/outfit.less index b97c354c..bccd47f8 100755 --- a/src/less/outfit.less +++ b/src/less/outfit.less @@ -190,11 +190,25 @@ }); } + &.quarter { + width: 25%; + + .tablet({ + td { + line-height: 2em; + } + }); + + .smallTablet({ + width: 50% !important; + }); + } + &.third { width: 33%; .smallTablet({ - width: 100% !important; + width: 50% !important; }); } From 73d609610a66d1e6a4a320283acc99dc0c941756 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Tue, 14 Mar 2017 08:29:09 +0000 Subject: [PATCH 20/53] Fix axis for effectice shield --- src/app/components/Defence.jsx | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index cb9bdf82..5a0566b1 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -55,6 +55,7 @@ export default class Defence extends TranslatedComponent { */ _calcMetrics(ship, opponent, sys) { const sysResistance = this._calcSysResistance(sys); + const maxSysResistance = this._calcSysResistance(4); // Obtain the opponent's sustained DPS for later damage calculations // const opponentSDps = Calc.sustainedDps(opponent, range); @@ -109,28 +110,32 @@ export default class Defence extends TranslatedComponent { generator: 1, boosters: 1, sys: 1 - sysResistance, - total: 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) + 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) + 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) + total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance), + max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance) }; shielddamage.absolutesdps = opponentSDps.absolute *= shield.absolute.total; @@ -239,6 +244,7 @@ export default class Defence extends TranslatedComponent { 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') }); @@ -277,6 +283,9 @@ export default class Defence extends TranslatedComponent { 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') }); + +console.log(`max effective shields are ${shield.absolute.max}/${shield.explosive.max}/${shield.kinetic.max}/${shield.thermal.max}`); + 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 = []; @@ -336,14 +345,14 @@ export default class Defence extends TranslatedComponent {

    {translate('shield sources')}

    -
    -

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

    - -

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

    +
    +

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

    + +
    : null }
    @@ -354,14 +363,14 @@ export default class Defence extends TranslatedComponent {

    {translate('armour sources')}

    -
    -

    {translate('effective armour')}

    - -

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

    +
    +

    {translate('effective armour')}

    + +
    ); } } From 365810a610b810c6b67ad46b822edb402d85d223 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 15 Mar 2017 11:40:12 +0000 Subject: [PATCH 21/53] Working on shield recovery calculation --- src/app/components/BattleCentre.jsx | 4 +- src/app/components/Defence.jsx | 90 ++++++++++++++++++++++------- src/app/i18n/en.js | 7 +++ 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index d0f19138..251a61af 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -113,10 +113,10 @@ export default class BattleCentre extends TranslatedComponent { const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent } = this.state; const { ship } = this.props; - // Markers are used to propagate state changes without requiring a deep comparison of the ship + // 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; + const shieldMarker = '' + ship.shield + ':' + ship.shieldCells + ':' + ship.shieldExplRes + ':' + ship.shieldKinRes + ':' + ship.shieldThermRes + ':' + ship.armour + ship.standard[4].m.getSystemsCapacity() + ':' + ship.standard[4].m.getSystemsRechargeRate(); return ( diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 5a0566b1..0d13ad6c 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -29,8 +29,8 @@ export default class Defence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, shielddamage } = this._calcMetrics(props.ship, props.opponent, props.sys); - this.state = { shield, armour, shielddamage }; + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(props.ship, props.opponent, props.sys); + this.state = { shield, armour, shielddamage, armourdamage }; } /** @@ -40,8 +40,8 @@ export default class Defence extends TranslatedComponent { */ componentWillReceiveProps(nextProps) { if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { - const { shield, armour, shielddamage } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); - this.setState({ shield, armour, shielddamage }); + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys); + this.setState({ shield, armour, shielddamage, armourdamage }); return true; } } @@ -97,11 +97,49 @@ export default class Defence extends TranslatedComponent { 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 15-second wait before shields start to recover + // Note that the shields use the broken regeneration rate to define how much energy goes in to the shields, and the normal + // regeneration rate to define how much energy is taken from the SYS capacitor + const shieldToRecover = (generatorStrength + boostersStrength) / 2; + const powerDistributor = ship.standard[4].m; + // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes + const capacitorDrain = shieldGenerator.getRegenerationRate() - powerDistributor.getSystemsRechargeRate() * (sys / 4); +console.log(`shieldToRecover is ${shieldToRecover}`); +console.log(`Regeneration rate is ${shieldGenerator.getRegenerationRate()}`); +console.log(`Power distributor recharge is ${powerDistributor.getSystemsRechargeRate() * sys / 4}`); +console.log(`capacitor drain is ${capacitorDrain}`); + const capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + +console.log(`Need to recover ${shieldToRecover}`); +console.log(`SYS contains ${powerDistributor.getSystemsCapacity()} and recharges at ${powerDistributor.getSystemsRechargeRate() * (sys / 4)}`); + let recover = 15; + if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) { + // We can recover the entire shield from the capacitor store + recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate(); +console.log(`We can recover the entire shield before the capacitor drains - takes ${recover}`); + } else { + // We can recover some of the shield from the capacitor store + recover += capacitorLifetime; +console.log(`We can recover ${capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()} before capacitor is empty`); +console.log(`Sys is ${sys}`); + const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); + if (sys === 0) { + // No system pips so will never recover shields +console.log(`Cannot recover shields`); + recover = Math.Inf; + } else { + // Recover remaining shields at the rate of the power distributor's recharge + recover += remainingShieldToRecover / (powerDistributor.getSystemsRechargeRate() * sys / 4); + } + } + shield = { generator: generatorStrength, boosters: boostersStrength, cells: ship.shieldCells, - total: generatorStrength + boostersStrength + ship.shieldCells + total: generatorStrength + boostersStrength + ship.shieldCells, + recover }; // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips. @@ -149,8 +187,8 @@ export default class Defence extends TranslatedComponent { const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost()); let armourReinforcement = 0; - let modulearmour = 0; - let moduleprotection = 1; + let moduleArmour = 0; + let moduleProtection = 1; let hullExplDmg = 1; let hullKinDmg = 1; @@ -168,11 +206,11 @@ export default class Defence extends TranslatedComponent { hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance()); } if (slot.m && slot.m.grp == 'mrp') { - modulearmour += slot.m.getIntegrity(); - moduleprotection = moduleprotection * (1 - slot.m.getProtection()); + moduleArmour += slot.m.getIntegrity(); + moduleProtection = moduleProtection * (1 - slot.m.getProtection()); } } - moduleprotection = 1 - moduleprotection; + moduleProtection = 1 - moduleProtection; // Apply diminishing returns hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2; @@ -182,12 +220,11 @@ export default class Defence extends TranslatedComponent { const armour = { bulkheads: armourBulkheads, reinforcement: armourReinforcement, - modulearmour: modulearmour, - moduleprotection: moduleprotection, + modulearmour: moduleArmour, + moduleprotection: moduleProtection, total: armourBulkheads + armourReinforcement }; - // Armour resistances have two components: bulkheads and HRPs // We re-cast these as damage percentages armour.absolute = { @@ -214,7 +251,15 @@ export default class Defence extends TranslatedComponent { total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg }; - return { shield, armour, shielddamage }; + const armourdamage = { + absolutesdps: opponentSDps.absolute *= armour.absolute.total, + explosivesdps: opponentSDps.explosive *= armour.explosive.total, + kineticsdps: opponentSDps.kinetic *= armour.kinetic.total, + thermalsdps: opponentSDps.thermal *= armour.thermal.total + }; + armourdamage.totalsdps = armourdamage.absolutesdps + armourdamage.explosivesdps + armourdamage.kineticsdps + armourdamage.thermalsdps; + + return { shield, armour, shielddamage, armourdamage }; } /** @@ -234,7 +279,7 @@ export default class Defence extends TranslatedComponent { const { ship, sys } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { shield, armour, shielddamage } = this.state; + const { shield, armour, shielddamage, armourdamage } = this.state; const shieldSourcesData = []; const effectiveShieldData = []; @@ -284,7 +329,6 @@ export default class Defence extends TranslatedComponent { shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic') }); shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal') }); -console.log(`max effective shields are ${shield.absolute.max}/${shield.explosive.max}/${shield.kinetic.max}/${shield.thermal.max}`); maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max); } @@ -329,17 +373,16 @@ console.log(`max effective shields are ${shield.absolute.max}/${shield.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') }); - // TODO versions of ship.calcShieldRecovery() and ship.calcShieldRecharge() that take account of SYS pips 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')} {formats.time(shield.total / shielddamage.totalsdps)}

    -

    {translate('PHRASE_TIME_TO_RECOVER_SHIELDS')} {formats.time(ship.calcShieldRecovery())}

    -

    {translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')} {formats.time(ship.calcShieldRecharge())}

    +

    {shieldTooltipDetails}

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

    {translate('PHRASE_TIME_TO_LOSE_SHIELDS')}
    {formats.time(shield.total / shielddamage.totalsdps)}

    +

    {translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
    {formats.time(shield.recover)}

    +

    {translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
    {formats.time(ship.calcShieldRecharge())}

    {translate('shield sources')}

    @@ -357,6 +400,11 @@ console.log(`max effective shields are ${shield.absolute.max}/${shield.explosive

    {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')}
    {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)}


    diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 74e2a3b2..45c4ab41 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -45,8 +45,15 @@ export const terms = { PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy', PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types', PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types', + PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for', + PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints', + PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules', TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', + TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', + TT_MODULE_ARMOUR: 'Armour protecting against module damage', + TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages', + TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', From 750d23b10a311d015d89a245be2ee907952e0edb Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Thu, 16 Mar 2017 07:35:52 +0000 Subject: [PATCH 22/53] Provide live sustained DPS for opponent --- src/app/components/BattleCentre.jsx | 8 +-- src/app/components/Defence.jsx | 105 +++++++++++++++++++++------- src/app/components/ShipPicker.jsx | 5 +- 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx index 251a61af..2ffdfd59 100644 --- a/src/app/components/BattleCentre.jsx +++ b/src/app/components/BattleCentre.jsx @@ -44,7 +44,7 @@ export default class BattleCentre extends TranslatedComponent { cargo: ship.cargoCapacity, boost: false, engagementRange: 1500, - opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots) + opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults) }; } @@ -110,13 +110,13 @@ export default class BattleCentre extends TranslatedComponent { render() { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent } = this.state; + 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(); + 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 ( @@ -136,7 +136,7 @@ export default class BattleCentre extends TranslatedComponent {

    {translate('defence')}

    - +
    ); diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 0d13ad6c..19a2e0c9 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -19,6 +19,7 @@ export default class Defence extends TranslatedComponent { marker: React.PropTypes.string.isRequired, ship: React.PropTypes.object.isRequired, opponent: React.PropTypes.object.isRequired, + engagementrange: React.PropTypes.number.isRequired, sys: React.PropTypes.number.isRequired }; @@ -29,7 +30,7 @@ export default class Defence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(props.ship, props.opponent, props.sys); + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(props.ship, props.opponent, props.sys, props.engagementrange); this.state = { shield, armour, shielddamage, armourdamage }; } @@ -40,31 +41,83 @@ export default class Defence extends TranslatedComponent { */ 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); + const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.engagementrange); this.setState({ shield, armour, shielddamage, armourdamage }); return true; } } /** - * Calculate shield metrics - * @param {Object} ship The ship - * @param {Object} opponent The opponent ship - * @param {int} sys The opponent ship - * @returns {Object} Shield metrics + * 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 */ - _calcMetrics(ship, opponent, sys) { + _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 }; + } + + /** + * 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 for later damage calculations - // const opponentSDps = Calc.sustainedDps(opponent, range); - const opponentSDps = { - absolute: 62.1, - explosive: 0, - kinetic: 7.4, - thermal: 7.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 = {}; @@ -176,10 +229,10 @@ console.log(`Cannot recover shields`); max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance) }; - shielddamage.absolutesdps = opponentSDps.absolute *= shield.absolute.total; - shielddamage.explosivesdps = opponentSDps.explosive *= shield.explosive.total; - shielddamage.kineticsdps = opponentSDps.kinetic *= shield.kinetic.total; - shielddamage.thermalsdps = opponentSDps.thermal *= shield.thermal.total; + 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; } @@ -252,10 +305,10 @@ console.log(`Cannot recover shields`); }; const armourdamage = { - absolutesdps: opponentSDps.absolute *= armour.absolute.total, - explosivesdps: opponentSDps.explosive *= armour.explosive.total, - kineticsdps: opponentSDps.kinetic *= armour.kinetic.total, - thermalsdps: opponentSDps.thermal *= armour.thermal.total + 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; @@ -380,7 +433,7 @@ console.log(`Cannot recover shields`);

    {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')}
    {formats.time(shield.total / shielddamage.totalsdps)}

    +

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

    {translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
    {formats.time(shield.recover)}

    {translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
    {formats.time(ship.calcShieldRecharge())}

    @@ -401,7 +454,7 @@ console.log(`Cannot recover shields`);

    {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')}
    {formats.time(armour.total / armourdamage.totalsdps)}

    +

    {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)}

    diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index 9f319a14..5b0529a5 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -18,7 +18,7 @@ export default class ShipPicker extends TranslatedComponent { }; static defaultProps = { - ship: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots) + ship: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults) } /** @@ -64,6 +64,9 @@ export default class ShipPicker extends TranslatedComponent { if (build) { // Ship is a particular build ship.buildFrom(Persist.getBuild(shipId, build)); + } else { + // Ship is a stock build + ship.buildWith(Ships[shipId].defaults); } this._closeMenu(); this.setState({ ship, build }); From 69489aa2673e5f1f8ffe28fe8862229f0121845a Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Thu, 16 Mar 2017 10:13:36 +0000 Subject: [PATCH 23/53] Update shortcut information --- src/app/components/Pips.jsx | 46 +++++++++++++++++++------------------ src/app/i18n/en.js | 5 ++++ 2 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index 2bec028e..ec8a0ff6 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -106,29 +106,31 @@ export default class Pips extends TranslatedComponent { * @param {Event} e Keyboard Event */ _keyDown(e) { - switch (e.keyCode) { - case 9: // Tab == boost - if (this.props.ship.canBoost()) { + 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._toggleBoost(); - } - break; - case 37: // Left arrow == increase SYS - e.preventDefault(); - this._incSys(); - break; - case 38: // Up arrow == increase ENG - e.preventDefault(); - this._incEng(); - break; - case 39: // Right arrow == increase WEP - e.preventDefault(); - this._incWep(); - break; - case 40: // Down arrow == reset - e.preventDefault(); - this._reset(); - break; + this._incSys(); + break; + case 38: // Up arrow == increase ENG + e.preventDefault(); + this._incEng(); + break; + case 39: // Right arrow == increase WEP + e.preventDefault(); + this._incWep(); + break; + case 40: // Down arrow == reset + e.preventDefault(); + this._reset(); + break; + } } } diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 45c4ab41..4d178e87 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -349,10 +349,15 @@ The damage received panel provides information about the effectiveness of your b

    Keyboard Shortcuts

    +
    Ctrl-b
    toggle boost
    Ctrl-e
    open export dialogue (outfitting page only)
    Ctrl-h
    open help dialogue
    Ctrl-i
    open import dialogue
    Ctrl-o
    open shortlink dialogue
    +
    Ctrl-left-arrow
    increase SYS capacitor
    +
    Ctrl-up-arrow
    increase ENG capacitor
    +
    Ctrl-right-arrow
    increase WEP capacitor
    +
    Ctrl-down-arrow
    reset power distributor
    Esc
    close any open dialogue

    Glossary

    From d60a8f2625fe230c59dff96966d57c6b120f3962 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Thu, 16 Mar 2017 12:05:15 +0000 Subject: [PATCH 24/53] Fix shield recovery/recharge calculations --- src/app/components/Defence.jsx | 72 ++++++++++++++++++++++----------- src/app/components/Movement.jsx | 4 +- src/app/i18n/en.js | 4 +- src/less/movement.less | 1 + 4 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 19a2e0c9..7531a91b 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -104,6 +104,16 @@ export default class Defence extends TranslatedComponent { 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 @@ -151,39 +161,55 @@ export default class Defence extends TranslatedComponent { 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 15-second wait before shields start to recover - // Note that the shields use the broken regeneration rate to define how much energy goes in to the shields, and the normal - // regeneration rate to define how much energy is taken from the SYS capacitor + // 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; - // Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes - const capacitorDrain = shieldGenerator.getRegenerationRate() - powerDistributor.getSystemsRechargeRate() * (sys / 4); -console.log(`shieldToRecover is ${shieldToRecover}`); -console.log(`Regeneration rate is ${shieldGenerator.getRegenerationRate()}`); -console.log(`Power distributor recharge is ${powerDistributor.getSystemsRechargeRate() * sys / 4}`); -console.log(`capacitor drain is ${capacitorDrain}`); - const capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain; + const sysRechargeRate = this._calcSysRechargeRate(powerDistributor, sys); -console.log(`Need to recover ${shieldToRecover}`); -console.log(`SYS contains ${powerDistributor.getSystemsCapacity()} and recharges at ${powerDistributor.getSystemsRechargeRate() * (sys / 4)}`); - let recover = 15; + // 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(); -console.log(`We can recover the entire shield before the capacitor drains - takes ${recover}`); } else { // We can recover some of the shield from the capacitor store recover += capacitorLifetime; -console.log(`We can recover ${capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()} before capacitor is empty`); -console.log(`Sys is ${sys}`); const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate(); if (sys === 0) { // No system pips so will never recover shields -console.log(`Cannot recover shields`); recover = Math.Inf; } else { // Recover remaining shields at the rate of the power distributor's recharge - recover += remainingShieldToRecover / (powerDistributor.getSystemsRechargeRate() * sys / 4); + 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); } } @@ -192,7 +218,8 @@ console.log(`Cannot recover shields`); boosters: boostersStrength, cells: ship.shieldCells, total: generatorStrength + boostersStrength + ship.shieldCells, - recover + recover, + recharge, }; // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips. @@ -419,7 +446,6 @@ console.log(`Cannot recover shields`); const effectiveThermalArmour = armour.total / armour.thermal.total; effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal') }); - // TODO these aren't updating when HRP metrics are altered (maybe - need to confirm) 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') }); @@ -433,9 +459,9 @@ console.log(`Cannot recover shields`);

    {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('infinity') : formats.time(shield.total / shielddamage.totalsdps)}

    -

    {translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}
    {formats.time(shield.recover)}

    -

    {translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}
    {formats.time(ship.calcShieldRecharge())}

    +

    {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')}

    diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index 62dbf544..e03f32c7 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -57,11 +57,11 @@ export default class Movement extends TranslatedComponent { // Speed - {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s // Pitch {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s // Roll - {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s // Yaw {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 4d178e87..1f7d8cb5 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -24,8 +24,8 @@ export const terms = { PHRASE_NO_BUILDS: 'No builds added to comparison!', PHRASE_NO_RETROCH: 'No Retrofitting changes', PHRASE_SELECT_BUILDS: 'Select builds to compare', - PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge', - PHRASE_SG_RECOVER: 'Time from 0% to 50% charge', + PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge, assuming full SYS capacitor to start with', + PHRASE_SG_RECOVER: 'Time from 0% to 50% charge, assuming full SYS capacitor to start with', PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target', diff --git a/src/less/movement.less b/src/less/movement.less index 724fae86..30eb083a 100644 --- a/src/less/movement.less +++ b/src/less/movement.less @@ -8,6 +8,7 @@ text { stroke: @primary; + font-size: 2em; } } } From 91cab5a4f1df2ec95e2d517a5505fd503b66e5da Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 17 Mar 2017 12:07:18 +0000 Subject: [PATCH 25/53] 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; From 7259a666eb0eb4cea3d43f2f9fa080ad4c51bacc Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 17 Mar 2017 12:34:36 +0000 Subject: [PATCH 26/53] Tidy-ups --- d3.min.js | 8 ++++---- package.json | 2 +- src/app/components/PieChart.jsx | 4 ++-- src/app/components/VerticalBarChart.jsx | 11 ++++++----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/d3.min.js b/d3.min.js index 10e71019..5b9a0d80 100644 --- a/d3.min.js +++ b/d3.min.js @@ -1,4 +1,4 @@ -!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports):"function"==typeof define&&define.amd?define(["exports"],n):n(t.d3=t.d3||{})}(this,function(t){"use strict";function n(t){return function(n,e){return so(t(n),e)}}function e(t,n,e){var i=Math.abs(n-t)/Math.max(0,e),r=Math.pow(10,Math.floor(Math.log(i)/Math.LN10)),o=i/r;return o>=po?r*=10:o>=yo?r*=5:o>=vo&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Au.exec(t))?pt(parseInt(n[1],16)):(n=Eu.exec(t))?new gt(n[1],n[2],n[3],1):(n=Uu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Lu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Pu.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Du.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Ru.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Fu.hasOwnProperty(t)?pt(Fu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Yu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/zu),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ou),a=Ct((.0193339*e+.119192*i+.9503041*r)/ju);return new Tt(116*u-16,500*(o-u),200*(u-a),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>Bu?Math.pow(t,1/3):t/$u+Xu}function St(t){return t>Iu?t*t*t:$u*(t-Xu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*qu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(ta*i+Qu*n-Ku*e)/(ta+Qu-Ku),o=i-r,u=(Gu*(e-r)-Wu*o)/Ju,a=Math.sqrt(u*u+o*o)/(Gu*r*(1-r)),s=a?Math.atan2(u,o)*qu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):oa(isNaN(t)?n:t)}function zt(t){return 1===(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):oa(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):oa(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?va:(na||(na=document.createElement("DIV"),ea=document.documentElement,ia=document.defaultView),na.style.transform=t,t=ia.getComputedStyle(ea.appendChild(na),null).getPropertyValue("transform"),ea.removeChild(na),t=t.slice(7,-1).split(","),ga(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?va:(ra||(ra=document.createElementNS("http://www.w3.org/2000/svg","g")),ra.setAttribute("transform",t),(t=ra.transform.baseVal.consolidate())?(t=t.matrix,ga(t.a,t.b,t.c,t.d,t.e,t.f)):va)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ha(t,r)},{i:s-2,x:ha(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ha(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ha(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ha(t,e)},{i:a-2,x:ha(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--Na}function Kt(){Aa=(Sa=Ua.now())+Ea,Na=ka=0;try{Qt()}finally{Na=0,nn(),Aa=0}}function tn(){var t=Ua.now(),n=t-Sa;n>Ca&&(Ea-=n,Sa=t)}function nn(){for(var t,n,e=wa,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:wa=n);ba=t,en(i)}function en(t){if(!Na){ka&&(ka=clearTimeout(ka));var n=t-Aa;n>24?(t<1/0&&(ka=setTimeout(Kt,n)),Ta&&(Ta=clearInterval(Ta))):(Ta||(Sa=Aa,Ta=setInterval(tn,Ca)),Na=1,La(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Fa)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>qa)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ya,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ya)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Ha)return Pa(r);f.state===za?(f.state=ja,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=au(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=au(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=au(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++ls}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return gs.time=Zt(),gs;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u;while((l=c<<1|h)===(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):Qs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(t){return nh=oh(t),eh=nh.format,ih=nh.formatPrefix,nh}function ue(){this.reset()}function ae(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function se(t){return t>1?0:t<-1?fh:Math.acos(t)}function he(t){return t>1?_h:t<-1?-_h:Math.asin(t)}function ce(){}function le(t){var n=t[0],e=t[1],i=mh(e);return[i*mh(n),i*Mh(n),Mh(e)]}function fe(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function _e(t){var n=Nh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function pe(t,n){return[t>fh?t-dh:t<-fh?t+dh:t,n]}function de(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function ye(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function we(t,n){return((t=t.x)[0]<0?t[1]-_h-lh:_h-t[1])-((n=n.x)[0]<0?n[1]-_h-lh:_h-n[1])}function be(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?fh:-fh,s=vh(o-e);vh(s-fh)0?_h:-_h),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=fh&&(vh(e-r)lh?gh((Mh(n)*(o=mh(i))*Mh(e)-Mh(i)*(r=mh(n))*Mh(t))/(r*o*u)):(n+i)/2}function Ne(t,n,e,i){var r;if(null==t)r=e*_h,i.point(-fh,r),i.point(0,r),i.point(fh,r),i.point(fh,0),i.point(fh,-r),i.point(0,-r),i.point(-fh,-r),i.point(-fh,0),i.point(-fh,r);else if(vh(t[0]-n[0])>lh){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ue(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Le(t,n){var e,i,r,o,u,a=new Ye(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=De);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Ye(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Fe)}function Pe(){return Le(this).eachBefore(Re)}function De(t){return t.children}function Re(t){t.data=t.data.data}function Fe(t){var n=0;do t.height=n;while((t=t.parent)&&t.height<++n)}function Ye(t){this.data=t,this.depth=this.height=0,this.parent=null}function qe(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function He(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,_=Math.max(f/y,y/l),_>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(n){}t._active=NaN,Ie(t)}function Ie(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function $e(t){return function(n,e){t(null==n?e:null)}}function Be(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Ve(t,n){return function(e){return t(e.responseText,n)}}function Ze(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==ic)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=ic;return t=null==t?[]:ec.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Qe(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Ke(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?ti:Ke,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Ge(t):t,h)))(+n)}var r,o,u,a=uc,s=uc,h=pa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,Je,c?Qe(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=nc.call(t,oc),e()):a.slice()},i.range=function(t){return arguments.length?(s=ec.call(t),e()):s.slice()},i.rangeRound=function(t){return s=ec.call(t),h=da,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ii(t){var n=t.domain;return t.ticks=function(t){var e=n();return go(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return ac(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ri(){var t=ei(Je,ha);return t.copy=function(){return ni(t,ri())},ii(t)}function oi(t,n,e,i){function r(n){return t(n=new Date((+n))),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do u.push(new Date((+e)));while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return sc.setTime(+n),hc.setTime(+i),t(sc),t(hc),Math.floor(e(sc,hc))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t===0}:function(n){return r.count(0,n)%t===0}):r:null}),r}function ui(t){return oi(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/dc})}function ai(t){return oi(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/dc})}function si(t){if(0<=t.y&&t.y<100){var n=new Date((-1),t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function hi(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function ci(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function li(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date((+e)));++a=s)return-1;if(r=n.charCodeAt(u++),37===r){if(r=n.charAt(u++),o=j[r in Ac?n.charAt(u++):r],!o||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=pi(k),U=di(k),L=pi(T),P=di(T),D=pi(C),R=di(C),F=pi(S),Y=di(S),q=pi(A),H=di(A),z={a:f,A:_,b:p,B:d,c:null,d:Ei,e:Ei,H:Ui,I:Li,j:Pi,L:Di,m:Ri,M:Fi,p:y,S:Yi,U:qi,w:Hi,W:zi,x:null,X:null,y:Oi,Y:ji,Z:Xi,"%":rr},O={a:v,A:g,b:x,B:m,c:null,d:Ii,e:Ii,H:$i,I:Bi,j:Vi,L:Zi,m:Wi,M:Ji,p:w,S:Gi,U:Qi,w:Ki,W:tr,x:null,X:null,y:nr,Y:er,Z:ir,"%":rr},j={a:o,A:u,b:a,B:s,c:h,d:Mi,e:Mi,H:ki,I:ki,j:Ni,L:Si,m:bi,M:Ti,p:r,S:Ci,U:vi,w:yi,W:gi,x:c,X:l,y:mi,Y:xi,Z:wi,"%":Ai};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",si);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,hi);return n.toString=function(){return t},n}}}function fi(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function wi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function bi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function Mi(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=Ec.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=Ec.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Ai(t,n,e){var i=Uc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ei(t,n){return fi(t.getDate(),n,2)}function Ui(t,n){return fi(t.getHours(),n,2)}function Li(t,n){return fi(t.getHours()%12||12,n,2)}function Pi(t,n){return fi(1+yc.count(xc(t),t),n,3)}function Di(t,n){return fi(t.getMilliseconds(),n,3)}function Ri(t,n){return fi(t.getMonth()+1,n,2)}function Fi(t,n){return fi(t.getMinutes(),n,2)}function Yi(t,n){return fi(t.getSeconds(),n,2)}function qi(t,n){return fi(vc.count(xc(t),t),n,2)}function Hi(t){return t.getDay()}function zi(t,n){return fi(gc.count(xc(t),t),n,2)}function Oi(t,n){return fi(t.getFullYear()%100,n,2)}function ji(t,n){return fi(t.getFullYear()%1e4,n,4)}function Xi(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+fi(n/60|0,"0",2)+fi(n%60,"0",2)}function Ii(t,n){return fi(t.getUTCDate(),n,2)}function $i(t,n){return fi(t.getUTCHours(),n,2)}function Bi(t,n){return fi(t.getUTCHours()%12||12,n,2)}function Vi(t,n){return fi(1+mc.count(Mc(t),t),n,3)}function Zi(t,n){return fi(t.getUTCMilliseconds(),n,3)}function Wi(t,n){return fi(t.getUTCMonth()+1,n,2)}function Ji(t,n){return fi(t.getUTCMinutes(),n,2)}function Gi(t,n){return fi(t.getUTCSeconds(),n,2)}function Qi(t,n){return fi(wc.count(Mc(t),t),n,2)}function Ki(t){return t.getUTCDay()}function tr(t,n){return fi(bc.count(Mc(t),t),n,2)}function nr(t,n){return fi(t.getUTCFullYear()%100,n,2)}function er(t,n){return fi(t.getUTCFullYear()%1e4,n,4)}function ir(){return"+0000"}function rr(){return"%"}function or(t){return Nc=li(t),kc=Nc.format,Tc=Nc.parse,Cc=Nc.utcFormat,Sc=Nc.utcParse,Nc}function ur(t){return t.toISOString()}function ar(t){var n=new Date(t);return isNaN(n)?null:n}function sr(t){this._context=t}function hr(t){return t[0]}function cr(t){return t[1]}function lr(t){this._curve=t}function fr(t){function n(n){return new lr(t(n))}return n._curve=t,n}function _r(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function pr(t){this._context=t}function dr(t,n){this._basis=new pr(t),this._beta=n}function yr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n){this._context=t,this._k=(1-n)/6}function xr(t,n){this._context=t,this._k=(1-n)/6}function mr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>Fc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>Fc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function wr(t,n){this._context=t,this._alpha=n}function br(t,n){this._context=t,this._alpha=n}function Mr(t,n){this._context=t,this._alpha=n}function Nr(t){return t<0?-1:1}function kr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(Nr(o)+Nr(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Tr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function Cr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Sr(t){this._context=t}function Ar(t){this._context=new Er(t)}function Er(t){this._context=t}function Ur(){this._=null}function Lr(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Pr(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Dr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Rr(t){for(;t.L;)t=t.L;return t}function Fr(t,n,e,i){var r=[null,null],o=Ic.push(r)-1;return r.left=t,r.right=n,e&&qr(r,t,n,e),i&&qr(r,n,t,i),jc[t.index].halfedges.push(o),jc[n.index].halfedges.push(o),r}function Yr(t,n,e){var i=[n,e];return i.left=t,i}function qr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Hr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function zr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Vc||Math.abs(r[0][1]-r[1][1])>Vc)||delete Ic[o]}function jr(t){return jc[t.index]={site:t,halfedges:[]}}function Xr(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function Ir(t,n){return n[+(n.left!==t.site)]}function $r(t,n){return n[+(n.left===t.site)]}function Br(){for(var t,n,e,i,r=0,o=jc.length;rVc||Math.abs(d-f)>Vc)&&(s.splice(a,0,Ic.push(Yr(u,_,Math.abs(p-t)Vc?[t,Math.abs(l-t)Vc?[Math.abs(f-i)Vc?[e,Math.abs(l-e)Vc?[Math.abs(f-n)=-Zc)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=$c.pop()||new Zr;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=Xc._;x;)if(v.yVc)a=a.L;else{if(r=o-io(a,u),!(r>Vc)){i>-Vc?(n=a.P,e=a):r>-Vc?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}jr(t);var s=Qr(t);if(Oc.insert(n,s),n||e){if(n===e)return Jr(n),e=Qr(n.site),Oc.insert(s,e),s.edge=e.edge=Fr(n.site,s.site),Wr(n),void Wr(e);if(!e)return void(s.edge=Fr(n.site,s.site));Jr(n),Jr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];qr(e.edge,h,p,m),s.edge=Fr(h,t,null,m),e.edge=Fr(t,p,null,m),Wr(n),Wr(e)}}function eo(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function io(t,n){var e=t.N;if(e)return eo(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function ro(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function oo(t,n){return n[1]-t[1]||n[0]-t[0]}function uo(t,n){var e,i,r,o=t.sort(oo).pop();for(Ic=[],jc=new Array(t.length),Oc=new Ur,Xc=new Ur;;)if(r=zc,o&&(!r||o[1]n?1:t>=n?0:NaN},ho=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},co=ho(so),lo=co.right,fo=Array.prototype,_o=(fo.slice,fo.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},mo=Array.prototype.slice,wo=function(t){return t},bo=1,Mo=2,No=3,ko=4,To=1e-6,Co={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Ao.hasOwnProperty(n)?{space:Ao[n],local:t}:t},Uo=function(t){var n=Eo(t);return(n.local?v:y)(n)},Lo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Po=document.documentElement;if(!Po.matches){var Do=Po.webkitMatchesSelector||Po.msMatchesSelector||Po.mozMatchesSelector||Po.oMatchesSelector;Lo=function(t){return function(){return Do.call(this,t)}}}}var Ro=Lo,Fo={},Yo=null;if("undefined"!=typeof document){var qo=document.documentElement;"onmouseenter"in qo||(Fo={mouseenter:"mouseover",mouseleave:"mouseout"})}var Ho=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=w&&(w=m+1);!(x=y[w])&&++w=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Ko=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):au(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},hu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){var n=this._names.indexOf(t);n<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var cu=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Yu=Math.PI/180,qu=180/Math.PI,Hu=18,zu=.95047,Ou=1,ju=1.08883,Xu=4/29,Iu=6/29,$u=3*Iu*Iu,Bu=Iu*Iu*Iu;bu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+Hu*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-Hu*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ou*St(t),n=zu*St(n),e=ju*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),bu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+Hu*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-Hu*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Vu=-.14861,Zu=1.78277,Wu=-.29227,Ju=-.90649,Gu=1.97294,Qu=Gu*Ju,Ku=Gu*Zu,ta=Zu*Wu-Ju*Vu;bu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?Nu:Math.pow(Nu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?Mu:Math.pow(Mu,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Yu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Vu*i+Zu*r)),255*(n+e*(Wu*i+Ju*r)),255*(n+e*(Gu*i)),this.opacity)}}));var na,ea,ia,ra,oa=function(t){return function(){return t}},ua=function Wc(t){function n(t,n){var i=e((t=vt(t)).r,(n=vt(n)).r),r=e(t.g,n.g),o=e(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=i(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var e=zt(t);return n.gamma=Wc,n}(1),aa=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ha(e,i)})),o=fa.lastIndex;return oqa&&e.statebs)if(Math.abs(c*a-s*h)>bs&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((ms-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>bs&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>bs||Math.abs(this._y1-h)>bs)&&(this._+="L"+s+","+h),e&&(l>Ms?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ws+ws),this._+="A"+e+","+e+",0,"+ +(l>=ms)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};var Ns="$";jn.prototype=Xn.prototype={constructor:jn,has:function(t){return Ns+t in this},get:function(t){return this[Ns+t]},set:function(t,n){return this[Ns+t]=n,this},remove:function(t){var n=Ns+t;return n in this&&delete this[n]},clear:function(){for(var t in this)t[0]===Ns&&delete this[t]},keys:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)n[0]===Ns&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)n[0]===Ns&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)n[0]===Ns&&++t;return t},empty:function(){for(var t in this)if(t[0]===Ns)return!1;return!0},each:function(t){for(var n in this)n[0]===Ns&&t(this[n],n.slice(1),this)}};var ks=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:ks.has,add:function(t){return t+="",this[Ns+t]=t,this},remove:ks.remove,clear:ks.clear,values:ks.keys,size:ks.size,empty:ks.empty,each:ks.each};var Ts=function(t){function n(t,n){var i,r,o=e(t,function(t,e){return i?i(t,e-1):(r=t,void(i=n?Vn(t,n):Bn(t)))});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do u=new Array(4),u[a]=h,h=u;while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do u=new Array(4),u[a]=h,h=u;while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},Ps=function(){var t=[];return this.visit(function(n){if(!n.length)do t.push(n.data);while(n=n.next)}),t},Ds=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Rs=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Fs=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Rs(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},qs=function(){return this._root},Hs=function(){var t=0;return this.visit(function(n){if(!n.length)do++t;while(n=n.next)}),t},zs=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Rs(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Rs(e,h,c,o,u)),(e=s[2])&&a.push(new Rs(e,i,c,h,u)),(e=s[1])&&a.push(new Rs(e,h,r,o,c)),(e=s[0])&&a.push(new Rs(e,i,r,h,c))}return this},Os=function(t){var n,e=[],i=[];for(this._root&&e.push(new Rs(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Rs(o,u,a,c,l)),(o=r[1])&&e.push(new Rs(o,c,a,s,l)),(o=r[2])&&e.push(new Rs(o,u,l,c,h)),(o=r[3])&&e.push(new Rs(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},js=function(t){return arguments.length?(this._x=t,this):this._x},Xs=function(t){return arguments.length?(this._y=t,this):this._y},Is=te.prototype=ne.prototype;Is.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Is.add=Us,Is.addAll=Jn,Is.cover=Ls,Is.data=Ps,Is.extent=Ds,Is.find=Fs,Is.remove=Ys,Is.removeAll=Gn,Is.root=qs,Is.size=Hs,Is.visit=zs,Is.visitAfter=Os,Is.x=js,Is.y=Xs;var $s,Bs=(Math.PI*(3-Math.sqrt(5)),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Vs=function(t){return t=Bs(Math.abs(t)),t?t[1]:NaN},Zs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ws=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},Js=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-($s=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Bs(t,Math.max(0,n+o-1))[0]},Gs=function(t,n){var e=Bs(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array((-r)).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},Qs={"":Ws,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Gs(100*t,n)},r:Gs,s:Js,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Ks=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,th=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var nh,eh,ih,rh=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],oh=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++ns||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=th(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=Qs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=th(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Vs(e)/3))),o=Math.pow(10,-r),u=rh[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Zs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};oe({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var uh=function(t){return Math.max(0,-Vs(Math.abs(t)))},ah=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Vs(n)/3)))-Vs(Math.abs(t)))},sh=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Vs(n)-Vs(t))+1},hh=function(){return new ue};ue.prototype={constructor:ue,reset:function(){this.s=this.t=0},add:function(t){ae(ch,t,this.t),ae(this,ch.s,this.s),this.s?this.t+=ch.t:this.s=ch.t},valueOf:function(){return this.s}};var ch=new ue,lh=1e-6,fh=Math.PI,_h=fh/2,ph=fh/4,dh=2*fh,yh=fh/180,vh=Math.abs,gh=Math.atan,xh=Math.atan2,mh=Math.cos,wh=(Math.ceil,Math.exp),bh=Math.log,Mh=(Math.pow,Math.sin),Nh=(Math.sign||function(t){return t>0?1:t<0?-1:0},Math.sqrt),kh=Math.tan;hh(),hh(),hh();pe.invert=pe;var Th=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:ce,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},Ch=function(t,n){return vh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},Ah=(hh(),hh(),hh(),1/0),Eh=-Ah;ve.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,dh)}},result:ce};hh();ge.prototype={_circle:xe(4.5),pointRadius:function(t){return this._circle=xe(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var Uh=hh(),Lh=function(t,n){var e=n[0],i=n[1],r=[Mh(e),-mh(e),0],o=0,u=0;Uh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>fh,T=p*m;if(Uh.add(xh(T*M*Mh(N),d*w+T*mh(N))),o+=k?b+M*dh:b,k^f>=e^g>=e){var C=fe(le(l),le(v));_e(C);var S=fe(r,C);_e(S);var A=(k^b>=0?-1:1)*he(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-lh||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(me))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=Th(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=xo(p);var t=Lh(_,v);p.length?(m||(o.polygonStart(),m=!0),Sh(p,we,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};Ph(function(){return!0},be,Ne,[-fh,-_h]);Te.prototype={constructor:Te,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};var Dh=(mh(30*yh),ke({point:function(t,n){this.stream.point(t*yh,n*yh)}}),Ce(function(t){return Nh(2/(1+t))}));Dh.invert=Se(function(t){return 2*he(t/2)});var Rh=Ce(function(t){return(t=se(t))&&t/Mh(t)});Rh.invert=Se(function(t){return t}),Ae.invert=function(t,n){return[-n,2*gh(wh(t))-_h]};var Fh=function(){return this.eachAfter(Ee)},Yh=function(t){var n,e,i,r,o=this,u=[o];do for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},Hh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Oh=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},jh=function(t){for(var n=this,e=Ue(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Xh=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Ih=function(){var t=[];return this.each(function(n){t.push(n)}),t},$h=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Bh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Ye.prototype=Le.prototype={constructor:Ye,count:Fh,each:Yh,eachAfter:Hh,eachBefore:qh,sum:zh,sort:Oh,path:jh,ancestors:Xh,descendants:Ih,leaves:$h,links:Bh,copy:Pe};var Vh=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?t:1)},n})(Wh);!function ul(t){function n(n,e,i,r,o){if((u=n._squarify)&&u.ratio===t)for(var u,a,s,h,c,l=-1,f=u.length,_=n.value;++l1?t:1)},n}(Wh);var Jh=([].slice,{}),Gh=function(t,n){function e(t){var n,e=h.status;if(!e&&Be(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(r){return void a.call("error",i,r)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=$e(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},Qh=function(t,n){return function(e,i){var r=Gh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};Qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),Qh("application/json",function(t){return JSON.parse(t.responseText)}),Qh("text/plain",function(t){return t.responseText}),Qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Kh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Gh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Ve(n,i=t)):i},o.row(i),r?o.get(r):o}};Kh("text/csv",Ss),Kh("text/tab-separated-values",Es);var tc=Array.prototype,nc=tc.map,ec=tc.slice,ic={name:"implicit"},rc=function(t){return function(){return t}},oc=function(t){return+t},uc=[0,1],ac=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=th(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=ah(a,s))||(i.precision=r),ih(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=sh(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=uh(a))||(i.precision=r-2*("%"===i.type))}return eh(i)},sc=new Date,hc=new Date,cc=oi(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});cc.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?oi(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):cc:null};var lc=1e3,fc=6e4,_c=36e5,pc=864e5,dc=6048e5,yc=(oi(function(t){t.setTime(Math.floor(t/lc)*lc)},function(t,n){t.setTime(+t+n*lc)},function(t,n){return(n-t)/lc},function(t){return t.getUTCSeconds()}),oi(function(t){t.setTime(Math.floor(t/fc)*fc)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getMinutes()}),oi(function(t){var n=t.getTimezoneOffset()*fc%_c;n<0&&(n+=_c),t.setTime(Math.floor((+t-n)/_c)*_c+n)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getHours()}),oi(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*fc)/pc},function(t){return t.getDate()-1})),vc=ui(0),gc=ui(1),xc=(ui(2),ui(3),ui(4),ui(5),ui(6),oi(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),oi(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));xc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var mc=(oi(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*fc)},function(t,n){return(n-t)/fc},function(t){return t.getUTCMinutes()}),oi(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+n*_c)},function(t,n){return(n-t)/_c},function(t){return t.getUTCHours()}),oi(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/pc},function(t){return t.getUTCDate()-1})),wc=ai(0),bc=ai(1),Mc=(ai(2),ai(3),ai(4),ai(5),ai(6),oi(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),oi(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));Mc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?oi(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var Nc,kc,Tc,Cc,Sc,Ac={"-":"",_:" ",0:"0"},Ec=/^\s*\d+/,Uc=/^%/,Lc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;or({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var Pc="%Y-%m-%dT%H:%M:%S.%LZ",Dc=(Date.prototype.toISOString?ur:Cc(Pc),+new Date("2000-01-01T00:00:00.000Z")?ar:Sc(Pc),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});Dc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),Dc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),Dc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),Dc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),Ma(Rt(300,.5,0),Rt(-240,.5,1));var Rc=(Ma(Rt(-100,.75,.35),Rt(80,1.5,.8)),Ma(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),Fc=1e-12;Math.PI;sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var Yc=function(t){return new sr(t)},qc=function(){function t(t){ -var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function al(t){function n(n){return 1===t?new pr(n):new dr(n,t)}return n.beta=function(t){return al(+t)},n}(.85),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:yr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function sl(t){function n(n){return new vr(n,t)}return n.tension=function(t){return sl(+t)},n}(0),gr.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function hl(t){function n(n){return new gr(n,t)}return n.tension=function(t){return hl(+t)},n}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:yr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function cl(t){function n(n){return new xr(n,t)}return n.tension=function(t){return cl(+t)},n}(0),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function ll(t){function n(n){return t?new wr(n,t):new vr(n,0)}return n.alpha=function(t){return ll(+t)},n}(.5),br.prototype={areaStart:Hc,areaEnd:Hc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function fl(t){function n(n){return t?new br(n,t):new gr(n,0)}return n.alpha=function(t){return fl(+t)},n}(.5),Mr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:mr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function _l(t){function n(n){return t?new Mr(n,t):new xr(n,0)}return n.alpha=function(t){return _l(+t)},n}(.5),Sr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:Cr(this,this._t0,Tr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,Cr(this,Tr(this,e=kr(this,t,n)),e);break;default:Cr(this,this._t0,e=kr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Ar.prototype=Object.create(Sr.prototype)).point=function(t,n){Sr.prototype.point.call(this,n,t)},Er.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ur.prototype={constructor:Ur,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Rr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Pr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Dr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Dr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Pr(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Rr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Pr(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Dr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Pr(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Dr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Pr(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Dr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var zc,Oc,jc,Xc,Ic,$c=[],Bc=[],Vc=1e-6,Zc=1e-12;uo.prototype={constructor:uo,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return Ir(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l=fo?r*=10:o>=_o?r*=5:o>=po&&(r*=2),n=0&&(e=t.slice(i+1),t=t.slice(0,i)),t&&!n.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}})}function p(t,n){for(var e,i=0,r=t.length;i=0&&(n=t.slice(e+1),t=t.slice(0,e)),{type:t,name:n}})}function w(t){return function(){var n=this.__on;if(n){for(var e,i=0,r=-1,o=n.length;in?1:t>=n?0:NaN}function A(t){return function(){this.removeAttribute(t)}}function E(t){return function(){this.removeAttributeNS(t.space,t.local)}}function U(t,n){return function(){this.setAttribute(t,n)}}function L(t,n){return function(){this.setAttributeNS(t.space,t.local,n)}}function P(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttribute(t):this.setAttribute(t,e)}}function D(t,n){return function(){var e=n.apply(this,arguments);null==e?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,e)}}function R(t){return function(){this.style.removeProperty(t)}}function F(t,n,e){return function(){this.style.setProperty(t,n,e)}}function Y(t,n,e){return function(){var i=n.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,e)}}function q(t){return function(){delete this[t]}}function H(t,n){return function(){this[t]=n}}function z(t,n){return function(){var e=n.apply(this,arguments);null==e?delete this[t]:this[t]=e}}function O(t){return t.trim().split(/^|\s+/)}function j(t){return t.classList||new X(t)}function X(t){this._node=t,this._names=O(t.getAttribute("class")||"")}function I(t,n){for(var e=j(t),i=-1,r=n.length;++i>8&15|n>>4&240,n>>4&15|240&n,(15&n)<<4|15&n,1)):(n=Nu.exec(t))?pt(parseInt(n[1],16)):(n=ku.exec(t))?new gt(n[1],n[2],n[3],1):(n=Tu.exec(t))?new gt(255*n[1]/100,255*n[2]/100,255*n[3]/100,1):(n=Cu.exec(t))?dt(n[1],n[2],n[3],n[4]):(n=Su.exec(t))?dt(255*n[1]/100,255*n[2]/100,255*n[3]/100,n[4]):(n=Au.exec(t))?xt(n[1],n[2]/100,n[3]/100,1):(n=Eu.exec(t))?xt(n[1],n[2]/100,n[3]/100,n[4]):Uu.hasOwnProperty(t)?pt(Uu[t]):"transparent"===t?new gt(NaN,NaN,NaN,0):null}function pt(t){return new gt(t>>16&255,t>>8&255,255&t,1)}function dt(t,n,e,i){return i<=0&&(t=n=e=NaN),new gt(t,n,e,i)}function yt(t){return t instanceof ft||(t=_t(t)),t?(t=t.rgb(),new gt(t.r,t.g,t.b,t.opacity)):new gt}function vt(t,n,e,i){return 1===arguments.length?yt(t):new gt(t,n,e,null==i?1:i)}function gt(t,n,e,i){this.r=+t,this.g=+n,this.b=+e,this.opacity=+i}function xt(t,n,e,i){return i<=0?t=n=e=NaN:e<=0||e>=1?t=n=NaN:n<=0&&(t=NaN),new bt(t,n,e,i)}function mt(t){if(t instanceof bt)return new bt(t.h,t.s,t.l,t.opacity);if(t instanceof ft||(t=_t(t)),!t)return new bt;if(t instanceof bt)return t;t=t.rgb();var n=t.r/255,e=t.g/255,i=t.b/255,r=Math.min(n,e,i),o=Math.max(n,e,i),u=NaN,a=o-r,s=(o+r)/2;return a?(u=n===o?(e-i)/a+6*(e0&&s<1?0:u,new bt(u,a,s,t.opacity)}function wt(t,n,e,i){return 1===arguments.length?mt(t):new bt(t,n,e,null==i?1:i)}function bt(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Mt(t,n,e){return 255*(t<60?n+(e-n)*t/60:t<180?e:t<240?n+(e-n)*(240-t)/60:n)}function Nt(t){if(t instanceof Tt)return new Tt(t.l,t.a,t.b,t.opacity);if(t instanceof Pt){var n=t.h*Lu;return new Tt(t.l,Math.cos(n)*t.c,Math.sin(n)*t.c,t.opacity)}t instanceof gt||(t=yt(t));var e=Et(t.r),i=Et(t.g),r=Et(t.b),o=Ct((.4124564*e+.3575761*i+.1804375*r)/Du),u=Ct((.2126729*e+.7151522*i+.072175*r)/Ru);return new Tt(116*u-16,500*(o-u),200*(u-Ct((.0193339*e+.119192*i+.9503041*r)/Fu)),t.opacity)}function kt(t,n,e,i){return 1===arguments.length?Nt(t):new Tt(t,n,e,null==i?1:i)}function Tt(t,n,e,i){this.l=+t,this.a=+n,this.b=+e,this.opacity=+i}function Ct(t){return t>zu?Math.pow(t,1/3):t/Hu+Yu}function St(t){return t>qu?t*t*t:Hu*(t-Yu)}function At(t){return 255*(t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055)}function Et(t){return(t/=255)<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4)}function Ut(t){if(t instanceof Pt)return new Pt(t.h,t.c,t.l,t.opacity);t instanceof Tt||(t=Nt(t));var n=Math.atan2(t.b,t.a)*Pu;return new Pt(n<0?n+360:n,Math.sqrt(t.a*t.a+t.b*t.b),t.l,t.opacity)}function Lt(t,n,e,i){return 1===arguments.length?Ut(t):new Pt(t,n,e,null==i?1:i)}function Pt(t,n,e,i){this.h=+t,this.c=+n,this.l=+e,this.opacity=+i}function Dt(t){if(t instanceof Ft)return new Ft(t.h,t.s,t.l,t.opacity);t instanceof gt||(t=yt(t));var n=t.r/255,e=t.g/255,i=t.b/255,r=(Zu*i+Bu*n-Vu*e)/(Zu+Bu-Vu),o=i-r,u=($u*(e-r)-Xu*o)/Iu,a=Math.sqrt(u*u+o*o)/($u*r*(1-r)),s=a?Math.atan2(u,o)*Pu-120:NaN;return new Ft(s<0?s+360:s,a,r,t.opacity)}function Rt(t,n,e,i){return 1===arguments.length?Dt(t):new Ft(t,n,e,null==i?1:i)}function Ft(t,n,e,i){this.h=+t,this.s=+n,this.l=+e,this.opacity=+i}function Yt(t,n){return function(e){return t+e*n}}function qt(t,n,e){return t=Math.pow(t,e),n=Math.pow(n,e)-t,e=1/e,function(i){return Math.pow(t+i*n,e)}}function Ht(t,n){var e=n-t;return e?Yt(t,e>180||e<-180?e-360*Math.round(e/360):e):Ku(isNaN(t)?n:t)}function zt(t){return 1==(t=+t)?Ot:function(n,e){return e-n?qt(n,e,t):Ku(isNaN(n)?e:n)}}function Ot(t,n){var e=n-t;return e?Yt(t,e):Ku(isNaN(t)?n:t)}function jt(t){return function(){return t}}function Xt(t){return function(n){return t(n)+""}}function It(t){return"none"===t?la:(Wu||(Wu=document.createElement("DIV"),Ju=document.documentElement,Gu=document.defaultView),Wu.style.transform=t,t=Gu.getComputedStyle(Ju.appendChild(Wu),null).getPropertyValue("transform"),Ju.removeChild(Wu),t=t.slice(7,-1).split(","),fa(+t[0],+t[1],+t[2],+t[3],+t[4],+t[5]))}function $t(t){return null==t?la:(Qu||(Qu=document.createElementNS("http://www.w3.org/2000/svg","g")),Qu.setAttribute("transform",t),(t=Qu.transform.baseVal.consolidate())?(t=t.matrix,fa(t.a,t.b,t.c,t.d,t.e,t.f)):la)}function Bt(t,n,e,i){function r(t){return t.length?t.pop()+" ":""}function o(t,i,r,o,u,a){if(t!==r||i!==o){var s=u.push("translate(",null,n,null,e);a.push({i:s-4,x:ia(t,r)},{i:s-2,x:ia(i,o)})}else(r||o)&&u.push("translate("+r+n+o+e)}function u(t,n,e,o){t!==n?(t-n>180?n+=360:n-t>180&&(t+=360),o.push({i:e.push(r(e)+"rotate(",null,i)-2,x:ia(t,n)})):n&&e.push(r(e)+"rotate("+n+i)}function a(t,n,e,o){t!==n?o.push({i:e.push(r(e)+"skewX(",null,i)-2,x:ia(t,n)}):n&&e.push(r(e)+"skewX("+n+i)}function s(t,n,e,i,o,u){if(t!==e||n!==i){var a=o.push(r(o)+"scale(",null,",",null,")");u.push({i:a-4,x:ia(t,e)},{i:a-2,x:ia(n,i)})}else 1===e&&1===i||o.push(r(o)+"scale("+e+","+i+")")}return function(n,e){var i=[],r=[];return n=t(n),e=t(e),o(n.translateX,n.translateY,e.translateX,e.translateY,i,r),u(n.rotate,e.rotate,i,r),a(n.skewX,e.skewX,i,r),s(n.scaleX,n.scaleY,e.scaleX,e.scaleY,i,r),n=e=null,function(t){for(var n,e=-1,o=r.length;++e=0&&n._call.call(null,t),n=n._next;--ga}function Kt(){Ma=(ba=ka.now())+Na,ga=xa=0;try{Qt()}finally{ga=0,nn(),Ma=0}}function tn(){var t=ka.now(),n=t-ba;n>wa&&(Na-=n,ba=t)}function nn(){for(var t,n,e=da,i=1/0;e;)e._call?(i>e._time&&(i=e._time),t=e,e=e._next):(n=e._next,e._next=null,e=t?t._next=n:da=n);ya=t,en(i)}function en(t){if(!ga){xa&&(xa=clearTimeout(xa));var n=t-Ma;n>24?(t<1/0&&(xa=setTimeout(Kt,n)),ma&&(ma=clearInterval(ma))):(ma||(ba=Ma,ma=setInterval(tn,wa)),ga=1,Ta(Kt))}}function rn(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>Ea)throw new Error("too late");return e}function on(t,n){var e=t.__transition;if(!e||!(e=e[n])||e.state>La)throw new Error("too late");return e}function un(t,n){var e=t.__transition;if(!e||!(e=e[n]))throw new Error("too late");return e}function an(t,n,e){function i(t){e.state=Ua,e.timer.restart(r,e.delay,e.time),e.delay<=t&&r(t-e.delay)}function r(i){var h,c,l,f;if(e.state!==Ua)return u();for(h in s)if(f=s[h],f.name===e.name){if(f.state===Pa)return Ca(r);f.state===Da?(f.state=Fa,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete s[h]):+h=0&&(t=t.slice(0,n)),!t||"start"===t})}function kn(t,n,e){var i,r,o=Nn(n)?rn:on;return function(){var u=o(this,t),a=u.on;a!==i&&(r=(i=a).copy()).on(n,e),u.on=r}}function Tn(t){return function(){var n=this.parentNode;for(var e in this.__transition)if(+e!==t)return;n&&n.removeChild(this)}}function Cn(t,n){var e,i,r;return function(){var o=ru(this).getComputedStyle(this,null),u=o.getPropertyValue(t),a=(this.style.removeProperty(t),o.getPropertyValue(t));return u===a?null:u===e&&a===i?r:r=n(e=u,i=a)}}function Sn(t){return function(){this.style.removeProperty(t)}}function An(t,n,e){var i,r;return function(){var o=ru(this).getComputedStyle(this,null).getPropertyValue(t);return o===e?null:o===i?r:r=n(i=o,e)}}function En(t,n,e){var i,r,o;return function(){var u=ru(this).getComputedStyle(this,null),a=u.getPropertyValue(t),s=e(this);return null==s&&(this.style.removeProperty(t),s=u.getPropertyValue(t)),a===s?null:a===i&&s===r?o:o=n(i=a,r=s)}}function Un(t,n,e){function i(){var i=this,r=n.apply(i,arguments);return r&&function(n){i.style.setProperty(t,r(n),e)}}return i._value=n,i}function Ln(t){return function(){this.textContent=t}}function Pn(t){return function(){var n=t(this);this.textContent=null==n?"":n}}function Dn(t,n,e,i){this._groups=t,this._parents=n,this._name=e,this._id=i}function Rn(t){return ht().transition(t)}function Fn(){return++os}function Yn(t){return((t*=2)<=1?t*t*t:(t-=2)*t*t+2)/2}function qn(t,n){for(var e;!(e=t.__transition)||!(e=e[n]);)if(!(t=t.parentNode))return ss.time=Zt(),ss;return e}function Hn(t){return{type:t}}function zn(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function On(){return new zn}function jn(){}function Xn(t,n){var e=new jn;if(t instanceof jn)t.each(function(t,n){e.set(n,t)});else if(Array.isArray(t)){var i,r=-1,o=t.length;if(null==n)for(;++r=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u,r=_,!(_=_[l=c<<1|h]))return r[l]=p,t;if(a=+t._x.call(null,_.data),s=+t._y.call(null,_.data),n===a&&e===s)return p.next=_,r?r[l]=p:t._root=p,t;do{r=r?r[l]=new Array(4):t._root=new Array(4),(h=n>=(o=(d+v)/2))?d=o:v=o,(c=e>=(u=(y+g)/2))?y=u:g=u}while((l=c<<1|h)==(f=(s>=u)<<1|a>=o));return r[f]=_,r[l]=p,t}function Jn(t){var n,e,i,r,o=t.length,u=new Array(o),a=new Array(o),s=1/0,h=1/0,c=-(1/0),l=-(1/0);for(e=0;ec&&(c=i),rl&&(l=r));for(c",r=n[3]||"-",o=n[4]||"",u=!!n[5],a=n[6]&&+n[6],s=!!n[7],h=n[8]&&+n[8].slice(1),c=n[9]||"";"n"===c?(s=!0,c="g"):zs[c]||(c=""),(u||"0"===e&&"="===i)&&(u=!0,e="0",i="="),this.fill=e,this.align=i,this.sign=r,this.symbol=o,this.zero=u,this.width=a,this.comma=s,this.precision=h,this.type=c}function re(t){return t}function oe(){this.reset()}function ue(t,n,e){var i=t.s=n+e,r=i-n,o=i-r;t.t=n-o+(e-r)}function ae(t){return t>1?0:t<-1?th:Math.acos(t)}function se(t){return t>1?nh:t<-1?-nh:Math.asin(t)}function he(){}function ce(t){var n=t[0],e=t[1],i=sh(e);return[i*sh(n),i*lh(n),lh(e)]}function le(t,n){return[t[1]*n[2]-t[2]*n[1],t[2]*n[0]-t[0]*n[2],t[0]*n[1]-t[1]*n[0]]}function fe(t){var n=fh(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);t[0]/=n,t[1]/=n,t[2]/=n}function _e(t,n){return[t>th?t-ih:t<-th?t+ih:t,n]}function pe(t,n,e,i){this.x=t,this.z=n,this.o=e,this.e=i,this.v=!1,this.n=this.p=null}function de(t){if(n=t.length){for(var n,e,i=0,r=t[0];++i1}function me(t,n){return((t=t.x)[0]<0?t[1]-nh-Ks:nh-t[1])-((n=n.x)[0]<0?n[1]-nh-Ks:nh-n[1])}function we(t){var n,e=NaN,i=NaN,r=NaN;return{lineStart:function(){t.lineStart(),n=1},point:function(o,u){var a=o>0?th:-th,s=oh(o-e);oh(s-th)0?nh:-nh),t.point(r,i),t.lineEnd(),t.lineStart(),t.point(a,i),t.point(o,i),n=0):r!==a&&s>=th&&(oh(e-r)Ks?uh((lh(n)*(o=sh(i))*lh(e)-lh(i)*(r=sh(n))*lh(t))/(r*o*u)):(n+i)/2}function Me(t,n,e,i){var r;if(null==t)r=e*nh,i.point(-th,r),i.point(0,r),i.point(th,r),i.point(th,0),i.point(th,-r),i.point(0,-r),i.point(-th,-r),i.point(-th,0),i.point(-th,r);else if(oh(t[0]-n[0])>Ks){var o=t[0]=0;)n+=e[i].value;else n=1;t.value=n}function Ee(t,n){if(t===n)return t;var e=t.ancestors(),i=n.ancestors(),r=null;for(t=e.pop(),n=i.pop();t===n;)r=t,t=e.pop(),n=i.pop();return r}function Ue(t,n){var e,i,r,o,u,a=new Fe(t),s=+t.value&&(a.value=t.value),h=[a];for(null==n&&(n=Pe);e=h.pop();)if(s&&(e.value=+e.data.value),(r=n(e.data))&&(u=r.length))for(e.children=new Array(u),o=u-1;o>=0;--o)h.push(i=e.children[o]=new Fe(r[o])),i.parent=e,i.depth=e.depth+1;return a.eachBefore(Re)}function Le(){return Ue(this).eachBefore(De)}function Pe(t){return t.children}function De(t){t.data=t.data.data}function Re(t){var n=0;do{t.height=n}while((t=t.parent)&&t.height<++n)}function Fe(t){this.data=t,this.depth=this.height=0,this.parent=null}function Ye(t,n){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=n}function qe(t,n,e,i,r,o){for(var u,a,s,h,c,l,f,_,p,d,y,v=[],g=n.children,x=0,m=0,w=g.length,b=n.value;xf&&(f=a),y=c*c*d,(_=Math.max(f/y,y/l))>p){c-=a;break}p=_}v.push(u={value:c,dice:s=0;)if((e=t._tasks[i])&&(t._tasks[i]=null,e.abort))try{e.abort()}catch(t){}t._active=NaN,Xe(t)}function Xe(t){if(!t._active&&t._call){var n=t._data;t._data=void 0,t._call(t._error,n)}}function Ie(t){return function(n,e){t(null==n?e:null)}}function $e(t){var n=t.responseType;return n&&"text"!==n?t.response:t.responseText}function Be(t,n){return function(e){return t(e.responseText,n)}}function Ve(t){function n(n){var o=n+"",u=e.get(o);if(!u){if(r!==Xh)return r;e.set(o,u=i.push(n))}return t[(u-1)%t.length]}var e=Xn(),i=[],r=Xh;return t=null==t?[]:jh.call(t),n.domain=function(t){if(!arguments.length)return i.slice();i=[],e=Xn();for(var r,o,u=-1,a=t.length;++u=e?1:i(t)}}}function Ge(t){return function(n,e){var i=t(n=+n,e=+e);return function(t){return t<=0?n:t>=1?e:i(t)}}}function Qe(t,n,e,i){var r=t[0],o=t[1],u=n[0],a=n[1];return o2?Ke:Qe,o=u=null,i}function i(n){return(o||(o=r(a,s,c?Je(t):t,h)))(+n)}var r,o,u,a=Bh,s=Bh,h=sa,c=!1;return i.invert=function(t){return(u||(u=r(s,a,We,c?Ge(n):n)))(+t)},i.domain=function(t){return arguments.length?(a=Oh.call(t,$h),e()):a.slice()},i.range=function(t){return arguments.length?(s=jh.call(t),e()):s.slice()},i.rangeRound=function(t){return s=jh.call(t),h=ha,e()},i.clamp=function(t){return arguments.length?(c=!!t,e()):c},i.interpolate=function(t){return arguments.length?(h=t,e()):h},e()}function ei(t){var n=t.domain;return t.ticks=function(t){var e=n();return yo(e[0],e[e.length-1],null==t?10:t)},t.tickFormat=function(t,e){return Vh(n(),t,e)},t.nice=function(i){var r=n(),o=r.length-1,u=null==i?10:i,a=r[0],s=r[o],h=e(a,s,u);return h&&(h=e(Math.floor(a/h)*h,Math.ceil(s/h)*h,u),r[0]=Math.floor(a/h)*h,r[o]=Math.ceil(s/h)*h,n(r)),t},t}function ii(){var t=ni(We,ia);return t.copy=function(){return ti(t,ii())},ei(t)}function ri(t,n,e,i){function r(n){return t(n=new Date(+n)),n}return r.floor=r,r.ceil=function(e){return t(e=new Date(e-1)),n(e,1),t(e),e},r.round=function(t){var n=r(t),e=r.ceil(t);return t-n0))return u;do{u.push(new Date(+e))}while(n(e,o),t(e),e=n)for(;t(n),!e(n);)n.setTime(n-1)},function(t,i){if(t>=t)for(;--i>=0;)for(;n(t,1),!e(t););})},e&&(r.count=function(n,i){return Zh.setTime(+n),Wh.setTime(+i),t(Zh),t(Wh),Math.floor(e(Zh,Wh))},r.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?r.filter(i?function(n){return i(n)%t==0}:function(n){return r.count(0,n)%t==0}):r:null}),r}function oi(t){return ri(function(n){n.setDate(n.getDate()-(n.getDay()+7-t)%7),n.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+7*n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/Qh})}function ui(t){return ri(function(n){n.setUTCDate(n.getUTCDate()-(n.getUTCDay()+7-t)%7),n.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+7*n)},function(t,n){return(n-t)/Qh})}function ai(t){if(0<=t.y&&t.y<100){var n=new Date(-1,t.m,t.d,t.H,t.M,t.S,t.L);return n.setFullYear(t.y),n}return new Date(t.y,t.m,t.d,t.H,t.M,t.S,t.L)}function si(t){if(0<=t.y&&t.y<100){var n=new Date(Date.UTC(-1,t.m,t.d,t.H,t.M,t.S,t.L));return n.setUTCFullYear(t.y),n}return new Date(Date.UTC(t.y,t.m,t.d,t.H,t.M,t.S,t.L))}function hi(t){return{y:t,m:0,d:1,H:0,M:0,S:0,L:0}}function ci(t){function n(t,n){return function(e){var i,r,o,u=[],a=-1,s=0,h=t.length;for(e instanceof Date||(e=new Date(+e));++a=s)return-1;if(37===(r=n.charCodeAt(u++))){if(r=n.charAt(u++),!(o=j[r in fc?n.charAt(u++):r])||(i=o(t,e,i))<0)return-1}else if(r!=e.charCodeAt(i++))return-1}return i}function r(t,n,e){var i=E.exec(n.slice(e));return i?(t.p=U[i[0].toLowerCase()],e+i[0].length):-1}function o(t,n,e){var i=D.exec(n.slice(e));return i?(t.w=R[i[0].toLowerCase()],e+i[0].length):-1}function u(t,n,e){var i=L.exec(n.slice(e));return i?(t.w=P[i[0].toLowerCase()],e+i[0].length):-1}function a(t,n,e){var i=q.exec(n.slice(e));return i?(t.m=H[i[0].toLowerCase()],e+i[0].length):-1}function s(t,n,e){var i=F.exec(n.slice(e));return i?(t.m=Y[i[0].toLowerCase()],e+i[0].length):-1}function h(t,n,e){return i(t,b,n,e)}function c(t,n,e){return i(t,M,n,e)}function l(t,n,e){return i(t,N,n,e)}function f(t){return C[t.getDay()]}function _(t){return T[t.getDay()]}function p(t){return A[t.getMonth()]}function d(t){return S[t.getMonth()]}function y(t){return k[+(t.getHours()>=12)]}function v(t){return C[t.getUTCDay()]}function g(t){return T[t.getUTCDay()]}function x(t){return A[t.getUTCMonth()]}function m(t){return S[t.getUTCMonth()]}function w(t){return k[+(t.getUTCHours()>=12)]}var b=t.dateTime,M=t.date,N=t.time,k=t.periods,T=t.days,C=t.shortDays,S=t.months,A=t.shortMonths,E=_i(k),U=pi(k),L=_i(T),P=pi(T),D=_i(C),R=pi(C),F=_i(S),Y=pi(S),q=_i(A),H=pi(A),z={a:f,A:_,b:p,B:d,c:null,d:Ai,e:Ai,H:Ei,I:Ui,j:Li,L:Pi,m:Di,M:Ri,p:y,S:Fi,U:Yi,w:qi,W:Hi,x:null,X:null,y:zi,Y:Oi,Z:ji,"%":ir},O={a:v,A:g,b:x,B:m,c:null,d:Xi,e:Xi,H:Ii,I:$i,j:Bi,L:Vi,m:Zi,M:Wi,p:w,S:Ji,U:Gi,w:Qi,W:Ki,x:null,X:null,y:tr,Y:nr,Z:er,"%":ir},j={a:o,A:u,b:a,B:s,c:h,d:bi,e:bi,H:Ni,I:Ni,j:Mi,L:Ci,m:wi,M:ki,p:r,S:Ti,U:yi,w:di,W:vi,x:c,X:l,y:xi,Y:gi,Z:mi,"%":Si};return z.x=n(M,z),z.X=n(N,z),z.c=n(b,z),O.x=n(M,O),O.X=n(N,O),O.c=n(b,O),{format:function(t){var e=n(t+="",z);return e.toString=function(){return t},e},parse:function(t){var n=e(t+="",ai);return n.toString=function(){return t},n},utcFormat:function(t){var e=n(t+="",O);return e.toString=function(){return t},e},utcParse:function(t){var n=e(t,si);return n.toString=function(){return t},n}}}function li(t,n,e){var i=t<0?"-":"",r=(i?-t:t)+"",o=r.length;return i+(o68?1900:2e3),e+i[0].length):-1}function mi(t,n,e){var i=/^(Z)|([+-]\d\d)(?:\:?(\d\d))?/.exec(n.slice(e,e+6));return i?(t.Z=i[1]?0:-(i[2]+(i[3]||"00")),e+i[0].length):-1}function wi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.m=i[0]-1,e+i[0].length):-1}function bi(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.d=+i[0],e+i[0].length):-1}function Mi(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.m=0,t.d=+i[0],e+i[0].length):-1}function Ni(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.H=+i[0],e+i[0].length):-1}function ki(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.M=+i[0],e+i[0].length):-1}function Ti(t,n,e){var i=_c.exec(n.slice(e,e+2));return i?(t.S=+i[0],e+i[0].length):-1}function Ci(t,n,e){var i=_c.exec(n.slice(e,e+3));return i?(t.L=+i[0],e+i[0].length):-1}function Si(t,n,e){var i=pc.exec(n.slice(e,e+1));return i?e+i[0].length:-1}function Ai(t,n){return li(t.getDate(),n,2)}function Ei(t,n){return li(t.getHours(),n,2)}function Ui(t,n){return li(t.getHours()%12||12,n,2)}function Li(t,n){return li(1+Kh.count(ec(t),t),n,3)}function Pi(t,n){return li(t.getMilliseconds(),n,3)}function Di(t,n){return li(t.getMonth()+1,n,2)}function Ri(t,n){return li(t.getMinutes(),n,2)}function Fi(t,n){return li(t.getSeconds(),n,2)}function Yi(t,n){return li(tc.count(ec(t),t),n,2)}function qi(t){return t.getDay()}function Hi(t,n){return li(nc.count(ec(t),t),n,2)}function zi(t,n){return li(t.getFullYear()%100,n,2)}function Oi(t,n){return li(t.getFullYear()%1e4,n,4)}function ji(t){var n=t.getTimezoneOffset();return(n>0?"-":(n*=-1,"+"))+li(n/60|0,"0",2)+li(n%60,"0",2)}function Xi(t,n){return li(t.getUTCDate(),n,2)}function Ii(t,n){return li(t.getUTCHours(),n,2)}function $i(t,n){return li(t.getUTCHours()%12||12,n,2)}function Bi(t,n){return li(1+ic.count(uc(t),t),n,3)}function Vi(t,n){return li(t.getUTCMilliseconds(),n,3)}function Zi(t,n){return li(t.getUTCMonth()+1,n,2)}function Wi(t,n){return li(t.getUTCMinutes(),n,2)}function Ji(t,n){return li(t.getUTCSeconds(),n,2)}function Gi(t,n){return li(rc.count(uc(t),t),n,2)}function Qi(t){return t.getUTCDay()}function Ki(t,n){return li(oc.count(uc(t),t),n,2)}function tr(t,n){return li(t.getUTCFullYear()%100,n,2)}function nr(t,n){return li(t.getUTCFullYear()%1e4,n,4)}function er(){return"+0000"}function ir(){return"%"}function rr(t){return t.toISOString()}function or(t){var n=new Date(t);return isNaN(n)?null:n}function ur(t){this._context=t}function ar(t){return t[0]}function sr(t){return t[1]}function hr(t){this._curve=t}function cr(t){function n(n){return new hr(t(n))}return n._curve=t,n}function lr(t,n,e){t._context.bezierCurveTo((2*t._x0+t._x1)/3,(2*t._y0+t._y1)/3,(t._x0+2*t._x1)/3,(t._y0+2*t._y1)/3,(t._x0+4*t._x1+n)/6,(t._y0+4*t._y1+e)/6)}function fr(t){this._context=t}function _r(t,n){this._basis=new fr(t),this._beta=n}function pr(t,n,e){t._context.bezierCurveTo(t._x1+t._k*(t._x2-t._x0),t._y1+t._k*(t._y2-t._y0),t._x2+t._k*(t._x1-n),t._y2+t._k*(t._y1-e),t._x2,t._y2)}function dr(t,n){this._context=t,this._k=(1-n)/6}function yr(t,n){this._context=t,this._k=(1-n)/6}function vr(t,n){this._context=t,this._k=(1-n)/6}function gr(t,n,e){var i=t._x1,r=t._y1,o=t._x2,u=t._y2;if(t._l01_a>gc){var a=2*t._l01_2a+3*t._l01_a*t._l12_a+t._l12_2a,s=3*t._l01_a*(t._l01_a+t._l12_a);i=(i*a-t._x0*t._l12_2a+t._x2*t._l01_2a)/s,r=(r*a-t._y0*t._l12_2a+t._y2*t._l01_2a)/s}if(t._l23_a>gc){var h=2*t._l23_2a+3*t._l23_a*t._l12_a+t._l12_2a,c=3*t._l23_a*(t._l23_a+t._l12_a);o=(o*h+t._x1*t._l23_2a-n*t._l12_2a)/c,u=(u*h+t._y1*t._l23_2a-e*t._l12_2a)/c}t._context.bezierCurveTo(i,r,o,u,t._x2,t._y2)}function xr(t,n){this._context=t,this._alpha=n}function mr(t,n){this._context=t,this._alpha=n}function wr(t,n){this._context=t,this._alpha=n}function br(t){return t<0?-1:1}function Mr(t,n,e){var i=t._x1-t._x0,r=n-t._x1,o=(t._y1-t._y0)/(i||r<0&&-0),u=(e-t._y1)/(r||i<0&&-0),a=(o*r+u*i)/(i+r);return(br(o)+br(u))*Math.min(Math.abs(o),Math.abs(u),.5*Math.abs(a))||0}function Nr(t,n){var e=t._x1-t._x0;return e?(3*(t._y1-t._y0)/e-n)/2:n}function kr(t,n,e){var i=t._x0,r=t._y0,o=t._x1,u=t._y1,a=(o-i)/3;t._context.bezierCurveTo(i+a,r+a*n,o-a,u-a*e,o,u)}function Tr(t){this._context=t}function Cr(t){this._context=new Sr(t)}function Sr(t){this._context=t}function Ar(){this._=null}function Er(t){t.U=t.C=t.L=t.R=t.P=t.N=null}function Ur(t,n){var e=n,i=n.R,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.R=i.L,e.R&&(e.R.U=e),i.L=e}function Lr(t,n){var e=n,i=n.L,r=e.U;r?r.L===e?r.L=i:r.R=i:t._=i,i.U=r,e.U=i,e.L=i.R,e.L&&(e.L.U=e),i.R=e}function Pr(t){for(;t.L;)t=t.L;return t}function Dr(t,n,e,i){var r=[null,null],o=Tc.push(r)-1;return r.left=t,r.right=n,e&&Fr(r,t,n,e),i&&Fr(r,n,t,i),Nc[t.index].halfedges.push(o),Nc[n.index].halfedges.push(o),r}function Rr(t,n,e){var i=[n,e];return i.left=t,i}function Fr(t,n,e,i){t[0]||t[1]?t.left===e?t[1]=i:t[0]=i:(t[0]=i,t.left=n,t.right=e)}function Yr(t,n,e,i,r){var o,u=t[0],a=t[1],s=u[0],h=u[1],c=a[0],l=a[1],f=0,_=1,p=c-s,d=l-h;if(o=n-s,p||!(o>0)){if(o/=p,p<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=i-s,p||!(o<0)){if(o/=p,p<0){if(o>_)return;o>f&&(f=o)}else if(p>0){if(o0)){if(o/=d,d<0){if(o0){if(o>_)return;o>f&&(f=o)}if(o=r-h,d||!(o<0)){if(o/=d,d<0){if(o>_)return;o>f&&(f=o)}else if(d>0){if(o0||_<1)||(f>0&&(t[0]=[s+f*p,h+f*d]),_<1&&(t[1]=[s+_*p,h+_*d]),!0)}}}}}function qr(t,n,e,i,r){var o=t[1];if(o)return!0;var u,a,s=t[0],h=t.left,c=t.right,l=h[0],f=h[1],_=c[0],p=c[1],d=(l+_)/2,y=(f+p)/2;if(p===f){if(d=i)return;if(l>_){if(s){if(s[1]>=r)return}else s=[d,e];o=[d,r]}else{if(s){if(s[1]1)if(l>_){if(s){if(s[1]>=r)return}else s=[(e-a)/u,e];o=[(r-a)/u,r]}else{if(s){if(s[1]=i)return}else s=[n,u*n+a];o=[i,u*i+a]}else{if(s){if(s[0]Ac||Math.abs(r[0][1]-r[1][1])>Ac)||delete Tc[o]}function zr(t){return Nc[t.index]={site:t,halfedges:[]}}function Or(t,n){var e=t.site,i=n.left,r=n.right;return e===r&&(r=i,i=e),r?Math.atan2(r[1]-i[1],r[0]-i[0]):(e===i?(i=n[1],r=n[0]):(i=n[0],r=n[1]),Math.atan2(i[0]-r[0],r[1]-i[1]))}function jr(t,n){return n[+(n.left!==t.site)]}function Xr(t,n){return n[+(n.left===t.site)]}function Ir(){for(var t,n,e,i,r=0,o=Nc.length;rAc||Math.abs(d-f)>Ac)&&(s.splice(a,0,Tc.push(Rr(u,_,Math.abs(p-t)Ac?[t,Math.abs(l-t)Ac?[Math.abs(f-i)Ac?[e,Math.abs(l-e)Ac?[Math.abs(f-n)=-Ec)){var _=s*s+h*h,p=c*c+l*l,d=(l*_-h*p)/f,y=(s*p-c*_)/f,v=Cc.pop()||new Br;v.arc=t,v.site=r,v.x=d+u,v.y=(v.cy=y+a)+Math.sqrt(d*d+y*y),t.circle=v;for(var g=null,x=kc._;x;)if(v.yAc)a=a.L;else{if(!((r=o-no(a,u))>Ac)){i>-Ac?(n=a.P,e=a):r>-Ac?(n=a,e=a.N):n=e=a;break}if(!a.R){n=a;break}a=a.R}zr(t);var s=Jr(t);if(Mc.insert(n,s),n||e){if(n===e)return Zr(n),e=Jr(n.site),Mc.insert(s,e),s.edge=e.edge=Dr(n.site,s.site),Vr(n),void Vr(e);if(!e)return void(s.edge=Dr(n.site,s.site));Zr(n),Zr(e);var h=n.site,c=h[0],l=h[1],f=t[0]-c,_=t[1]-l,p=e.site,d=p[0]-c,y=p[1]-l,v=2*(f*y-_*d),g=f*f+_*_,x=d*d+y*y,m=[(y*g-_*x)/v+c,(f*x-d*g)/v+l];Fr(e.edge,h,p,m),s.edge=Dr(h,t,null,m),e.edge=Dr(t,p,null,m),Vr(n),Vr(e)}}function to(t,n){var e=t.site,i=e[0],r=e[1],o=r-n;if(!o)return i;var u=t.P;if(!u)return-(1/0);e=u.site;var a=e[0],s=e[1],h=s-n;if(!h)return a;var c=a-i,l=1/o-1/h,f=c/h;return l?(-f+Math.sqrt(f*f-2*l*(c*c/(-2*h)-s+h/2+r-o/2)))/l+i:(i+a)/2}function no(t,n){var e=t.N;if(e)return to(e,n);var i=t.site;return i[1]===n?i[0]:1/0}function eo(t,n,e){return(t[0]-e[0])*(n[1]-t[1])-(t[0]-n[0])*(e[1]-t[1])}function io(t,n){return n[1]-t[1]||n[0]-t[0]}function ro(t,n){var e,i,r,o=t.sort(io).pop();for(Tc=[],Nc=new Array(t.length),Mc=new Ar,kc=new Ar;;)if(r=bc,o&&(!r||o[1]n?1:t>=n?0:NaN},ao=function(t){return 1===t.length&&(t=n(t)),{left:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)<0?i=o+1:r=o}return i},right:function(n,e,i,r){for(null==i&&(i=0),null==r&&(r=n.length);i>>1;t(n[o],e)>0?r=o:i=o+1}return i}}},so=ao(uo),ho=so.right,co=Array.prototype,lo=(co.slice,co.map,function(t,n,e){t=+t,n=+n,e=(r=arguments.length)<2?(n=t,t=0,1):r<3?1:+e;for(var i=-1,r=0|Math.max(0,Math.ceil((n-t)/e)),o=new Array(r);++i=0;)for(i=t[r],n=i.length;--n>=0;)e[--u]=i[n];return e},go=Array.prototype.slice,xo=function(t){return t},mo=1,wo=2,bo=3,Mo=4,No=1e-6,ko={value:function(){}};f.prototype=l.prototype={constructor:f,on:function(t,n){var e,i=this._,r=_(t+"",i),o=-1,u=r.length;{if(!(arguments.length<2)){if(null!=n&&"function"!=typeof n)throw new Error("invalid callback: "+n);for(;++o0)for(var e,i,r=new Array(e),o=0;o=0&&"xmlns"!==(n=t.slice(0,e))&&(t=t.slice(e+1)),Co.hasOwnProperty(n)?{space:Co[n],local:t}:t},Ao=function(t){var n=So(t);return(n.local?v:y)(n)},Eo=function(t){return function(){return this.matches(t)}};if("undefined"!=typeof document){var Uo=document.documentElement;if(!Uo.matches){var Lo=Uo.webkitMatchesSelector||Uo.msMatchesSelector||Uo.mozMatchesSelector||Uo.oMatchesSelector;Eo=function(t){return function(){return Lo.call(this,t)}}}}var Po=Eo,Do={},Ro=null;if("undefined"!=typeof document){"onmouseenter"in document.documentElement||(Do={mouseenter:"mouseover",mouseleave:"mouseout"})}var Fo=function(t,n,e){var i,r,o=m(t+""),u=o.length;{if(!(arguments.length<2)){for(a=n?b:w,null==e&&(e=!1),i=0;i=m&&(m=x+1);!(g=y[m])&&++m=0;)(i=r[o])&&(u&&u!==i.nextSibling&&u.parentNode.insertBefore(i,u),u=i);return this},Jo=function(t){function n(n,e){return n&&e?t(n.__data__,e.__data__):!n-!e}t||(t=S);for(var e=this._groups,i=e.length,r=new Array(i),o=0;o1?this.each((null==n?R:"function"==typeof n?Y:F)(t,n,null==e?"":e)):ru(i=this.node()).getComputedStyle(i,null).getPropertyValue(t)},uu=function(t,n){return arguments.length>1?this.each((null==n?q:"function"==typeof n?z:H)(t,n)):this.node()[t]};X.prototype={add:function(t){this._names.indexOf(t)<0&&(this._names.push(t),this._node.setAttribute("class",this._names.join(" ")))},remove:function(t){var n=this._names.indexOf(t);n>=0&&(this._names.splice(n,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var au=function(t,n){var e=O(t+"");if(arguments.length<2){for(var i=j(this.node()),r=-1,o=e.length;++r=240?t-240:t+120,r,i),Mt(t,r,i),Mt(t<120?t+240:t-120,r,i),this.opacity)},displayable:function(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1}}));var Lu=Math.PI/180,Pu=180/Math.PI,Du=.95047,Ru=1,Fu=1.08883,Yu=4/29,qu=6/29,Hu=3*qu*qu,zu=qu*qu*qu;xu(Tt,kt,lt(ft,{brighter:function(t){return new Tt(this.l+18*(null==t?1:t),this.a,this.b,this.opacity)},darker:function(t){return new Tt(this.l-18*(null==t?1:t),this.a,this.b,this.opacity)},rgb:function(){var t=(this.l+16)/116,n=isNaN(this.a)?t:t+this.a/500,e=isNaN(this.b)?t:t-this.b/200;return t=Ru*St(t),n=Du*St(n),e=Fu*St(e),new gt(At(3.2404542*n-1.5371385*t-.4985314*e),At(-.969266*n+1.8760108*t+.041556*e),At(.0556434*n-.2040259*t+1.0572252*e),this.opacity)}})),xu(Pt,Lt,lt(ft,{brighter:function(t){return new Pt(this.h,this.c,this.l+18*(null==t?1:t),this.opacity)},darker:function(t){return new Pt(this.h,this.c,this.l-18*(null==t?1:t),this.opacity)},rgb:function(){return Nt(this).rgb()}}));var Ou=-.14861,ju=1.78277,Xu=-.29227,Iu=-.90649,$u=1.97294,Bu=$u*Iu,Vu=$u*ju,Zu=ju*Xu-Iu*Ou;xu(Ft,Rt,lt(ft,{brighter:function(t){return t=null==t?1/.7:Math.pow(1/.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},darker:function(t){return t=null==t?.7:Math.pow(.7,t),new Ft(this.h,this.s,this.l*t,this.opacity)},rgb:function(){var t=isNaN(this.h)?0:(this.h+120)*Lu,n=+this.l,e=isNaN(this.s)?0:this.s*n*(1-n),i=Math.cos(t),r=Math.sin(t);return new gt(255*(n+e*(Ou*i+ju*r)),255*(n+e*(Xu*i+Iu*r)),255*(n+e*($u*i)),this.opacity)}}));var Wu,Ju,Gu,Qu,Ku=function(t){return function(){return t}},ta=function t(n){function e(t,n){var e=i((t=vt(t)).r,(n=vt(n)).r),r=i(t.g,n.g),o=i(t.b,n.b),u=Ot(t.opacity,n.opacity);return function(n){return t.r=e(n),t.g=r(n),t.b=o(n),t.opacity=u(n),t+""}}var i=zt(n);return e.gamma=t,e}(1),na=function(t,n){var e,i=n?n.length:0,r=t?Math.min(i,t.length):0,o=new Array(i),u=new Array(i);for(e=0;eo&&(r=n.slice(o,r),a[u]?a[u]+=r:a[++u]=r),(e=e[0])===(i=i[0])?a[u]?a[u]+=i:a[++u]=i:(a[++u]=null,s.push({i:u,x:ia(e,i)})),o=ua.lastIndex;return oLa&&e.state1e-6)if(Math.abs(c*a-s*h)>1e-6&&r){var f=e-o,_=i-u,p=a*a+s*s,d=f*f+_*_,y=Math.sqrt(p),v=Math.sqrt(l),g=r*Math.tan((cs-Math.acos((p+l-d)/(2*y*v)))/2),x=g/v,m=g/y;Math.abs(x-1)>1e-6&&(this._+="L"+(t+x*h)+","+(n+x*c)),this._+="A"+r+","+r+",0,0,"+ +(c*f>h*_)+","+(this._x1=t+m*a)+","+(this._y1=n+m*s)}else this._+="L"+(this._x1=t)+","+(this._y1=n);else;},arc:function(t,n,e,i,r,o){t=+t,n=+n,e=+e;var u=e*Math.cos(i),a=e*Math.sin(i),s=t+u,h=n+a,c=1^o,l=o?i-r:r-i;if(e<0)throw new Error("negative radius: "+e);null===this._x1?this._+="M"+s+","+h:(Math.abs(this._x1-s)>1e-6||Math.abs(this._y1-h)>1e-6)&&(this._+="L"+s+","+h),e&&(l>fs?this._+="A"+e+","+e+",0,1,"+c+","+(t-u)+","+(n-a)+"A"+e+","+e+",0,1,"+c+","+(this._x1=s)+","+(this._y1=h):(l<0&&(l=l%ls+ls),this._+="A"+e+","+e+",0,"+ +(l>=cs)+","+c+","+(this._x1=t+e*Math.cos(r))+","+(this._y1=n+e*Math.sin(r))))},rect:function(t,n,e,i){this._+="M"+(this._x0=this._x1=+t)+","+(this._y0=this._y1=+n)+"h"+ +e+"v"+ +i+"h"+-e+"Z"},toString:function(){return this._}};jn.prototype=Xn.prototype={constructor:jn,has:function(t){return"$"+t in this},get:function(t){return this["$"+t]},set:function(t,n){return this["$"+t]=n,this},remove:function(t){var n="$"+t;return n in this&&delete this[n]},clear:function(){for(var t in this)"$"===t[0]&&delete this[t]},keys:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(n.slice(1));return t},values:function(){var t=[];for(var n in this)"$"===n[0]&&t.push(this[n]);return t},entries:function(){var t=[];for(var n in this)"$"===n[0]&&t.push({key:n.slice(1),value:this[n]});return t},size:function(){var t=0;for(var n in this)"$"===n[0]&&++t;return t},empty:function(){for(var t in this)if("$"===t[0])return!1;return!0},each:function(t){for(var n in this)"$"===n[0]&&t(this[n],n.slice(1),this)}};var _s=Xn.prototype;In.prototype=$n.prototype={constructor:In,has:_s.has,add:function(t){return t+="",this["$"+t]=t,this},remove:_s.remove,clear:_s.clear,values:_s.keys,size:_s.size,empty:_s.empty,each:_s.each};var ps=function(t){function n(t,n){var i,r,o=e(t,function(t,e){if(i)return i(t,e-1);r=t,i=n?Vn(t,n):Bn(t)});return o.columns=r,o}function e(t,n){function e(){if(c>=h)return u;if(r)return r=!1,o;var n,e=c;if(34===t.charCodeAt(e)){for(var i=e;i++t||t>r||i>n||n>o))return this;var u,a,s=r-e,h=this._root;switch(a=(n<(i+o)/2)<<1|t<(e+r)/2){case 0:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,o=i+s,t>r||n>o);break;case 1:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,o=i+s,e>t||n>o);break;case 2:do{u=new Array(4),u[a]=h,h=u}while(s*=2,r=e+s,i=o-s,t>r||i>n);break;case 3:do{u=new Array(4),u[a]=h,h=u}while(s*=2,e=r-s,i=o-s,e>t||i>n)}this._root&&this._root.length&&(this._root=h)}return this._x0=e,this._y0=i,this._x1=r,this._y1=o,this},ws=function(){var t=[];return this.visit(function(n){if(!n.length)do{t.push(n.data)}while(n=n.next)}),t},bs=function(t){return arguments.length?this.cover(+t[0][0],+t[0][1]).cover(+t[1][0],+t[1][1]):isNaN(this._x0)?void 0:[[this._x0,this._y0],[this._x1,this._y1]]},Ms=function(t,n,e,i,r){this.node=t,this.x0=n,this.y0=e,this.x1=i,this.y1=r},Ns=function(t,n,e){var i,r,o,u,a,s,h,c=this._x0,l=this._y0,f=this._x1,_=this._y1,p=[],d=this._root;for(d&&p.push(new Ms(d,c,l,f,_)),null==e?e=1/0:(c=t-e,l=n-e,f=t+e,_=n+e,e*=e);s=p.pop();)if(!(!(d=s.node)||(r=s.x0)>f||(o=s.y0)>_||(u=s.x1)=v)<<1|t>=y)&&(s=p[p.length-1],p[p.length-1]=p[p.length-1-h],p[p.length-1-h]=s)}else{var g=t-+this._x.call(null,d.data),x=n-+this._y.call(null,d.data),m=g*g+x*x;if(m=(a=(p+y)/2))?p=a:y=a,(c=u>=(s=(d+v)/2))?d=s:v=s,n=_,!(_=_[l=c<<1|h]))return this;if(!_.length)break;(n[l+1&3]||n[l+2&3]||n[l+3&3])&&(e=n,f=l)}for(;_.data!==t;)if(i=_,!(_=_.next))return this;return(r=_.next)&&delete _.next,i?(r?i.next=r:delete i.next,this):n?(r?n[l]=r:delete n[l],(_=n[0]||n[1]||n[2]||n[3])&&_===(n[3]||n[2]||n[1]||n[0])&&!_.length&&(e?e[f]=_:this._root=_),this):(this._root=r,this)},Ts=function(){return this._root},Cs=function(){var t=0;return this.visit(function(n){if(!n.length)do{++t}while(n=n.next)}),t},Ss=function(t){var n,e,i,r,o,u,a=[],s=this._root;for(s&&a.push(new Ms(s,this._x0,this._y0,this._x1,this._y1));n=a.pop();)if(!t(s=n.node,i=n.x0,r=n.y0,o=n.x1,u=n.y1)&&s.length){var h=(i+o)/2,c=(r+u)/2;(e=s[3])&&a.push(new Ms(e,h,c,o,u)),(e=s[2])&&a.push(new Ms(e,i,c,h,u)),(e=s[1])&&a.push(new Ms(e,h,r,o,c)),(e=s[0])&&a.push(new Ms(e,i,r,h,c))}return this},As=function(t){var n,e=[],i=[];for(this._root&&e.push(new Ms(this._root,this._x0,this._y0,this._x1,this._y1));n=e.pop();){var r=n.node;if(r.length){var o,u=n.x0,a=n.y0,s=n.x1,h=n.y1,c=(u+s)/2,l=(a+h)/2;(o=r[0])&&e.push(new Ms(o,u,a,c,l)),(o=r[1])&&e.push(new Ms(o,c,a,s,l)),(o=r[2])&&e.push(new Ms(o,u,l,c,h)),(o=r[3])&&e.push(new Ms(o,c,l,s,h))}i.push(n)}for(;n=i.pop();)t(n.node,n.x0,n.y0,n.x1,n.y1);return this},Es=function(t){return arguments.length?(this._x=t,this):this._x},Us=function(t){return arguments.length?(this._y=t,this):this._y},Ls=te.prototype=ne.prototype;Ls.copy=function(){var t,n,e=new ne(this._x,this._y,this._x0,this._y0,this._x1,this._y1),i=this._root;if(!i)return e;if(!i.length)return e._root=ee(i),e;for(t=[{source:i,target:e._root=new Array(4)}];i=t.pop();)for(var r=0;r<4;++r)(n=i.source[r])&&(n.length?t.push({source:n,target:i.target[r]=new Array(4)}):i.target[r]=ee(n));return e},Ls.add=xs,Ls.addAll=Jn,Ls.cover=ms,Ls.data=ws,Ls.extent=bs,Ls.find=Ns,Ls.remove=ks,Ls.removeAll=Gn,Ls.root=Ts,Ls.size=Cs,Ls.visit=Ss,Ls.visitAfter=As,Ls.x=Es,Ls.y=Us;var Ps,Ds=(Math.PI,Math.sqrt(5),function(t,n){if((e=(t=n?t.toExponential(n-1):t.toExponential()).indexOf("e"))<0)return null;var e,i=t.slice(0,e);return[i.length>1?i[0]+i.slice(2):i,+t.slice(e+1)]}),Rs=function(t){return t=Ds(Math.abs(t)),t?t[1]:NaN},Fs=function(t,n){return function(e,i){for(var r=e.length,o=[],u=0,a=t[0],s=0;r>0&&a>0&&(s+a+1>i&&(a=Math.max(1,i-s)),o.push(e.substring(r-=a,r+a)),!((s+=a+1)>i));)a=t[u=(u+1)%t.length];return o.reverse().join(n)}},Ys=function(t,n){t=t.toPrecision(n);t:for(var e,i=t.length,r=1,o=-1;r0&&(o=0)}return o>0?t.slice(0,o)+t.slice(e+1):t},qs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1],o=r-(Ps=3*Math.max(-8,Math.min(8,Math.floor(r/3))))+1,u=i.length;return o===u?i:o>u?i+new Array(o-u+1).join("0"):o>0?i.slice(0,o)+"."+i.slice(o):"0."+new Array(1-o).join("0")+Ds(t,Math.max(0,n+o-1))[0]},Hs=function(t,n){var e=Ds(t,n);if(!e)return t+"";var i=e[0],r=e[1];return r<0?"0."+new Array(-r).join("0")+i:i.length>r+1?i.slice(0,r+1)+"."+i.slice(r+1):i+new Array(r-i.length+2).join("0")},zs={"":Ys,"%":function(t,n){return(100*t).toFixed(n)},b:function(t){return Math.round(t).toString(2)},c:function(t){return t+""},d:function(t){return Math.round(t).toString(10)},e:function(t,n){return t.toExponential(n)},f:function(t,n){return t.toFixed(n)},g:function(t,n){return t.toPrecision(n)},o:function(t){return Math.round(t).toString(8)},p:function(t,n){return Hs(100*t,n)},r:Hs,s:qs,X:function(t){return Math.round(t).toString(16).toUpperCase()},x:function(t){return Math.round(t).toString(16)}},Os=/^(?:(.)?([<>=^]))?([+\-\( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?([a-z%])?$/i,js=function(t){return new ie(t)};ie.prototype.toString=function(){return this.fill+this.align+this.sign+this.symbol+(this.zero?"0":"")+(null==this.width?"":Math.max(1,0|this.width))+(this.comma?",":"")+(null==this.precision?"":"."+Math.max(0,0|this.precision))+this.type};var Xs,Is,$s,Bs=["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"],Vs=function(t){function n(t){function n(t){var n,r,s,g=p,x=d;if("c"===_)x=y(t)+x,t="";else{t=+t;var m=(t<0||1/t<0)&&(t*=-1,!0);if(t=y(t,f),m)for(n=-1,r=t.length,m=!1;++n(s=t.charCodeAt(n))||s>57){x=(46===s?o+t.slice(n+1):t.slice(n))+x,t=t.slice(0,n);break}}l&&!h&&(t=i(t,1/0));var w=g.length+t.length+x.length,b=w>1)+g+t+x+b.slice(w)}return b+g+t+x}t=js(t);var e=t.fill,u=t.align,a=t.sign,s=t.symbol,h=t.zero,c=t.width,l=t.comma,f=t.precision,_=t.type,p="$"===s?r[0]:"#"===s&&/[boxX]/.test(_)?"0"+_.toLowerCase():"",d="$"===s?r[1]:/[%p]/.test(_)?"%":"",y=zs[_],v=!_||/[defgprs%]/.test(_);return f=null==f?_?6:12:/[gprs]/.test(_)?Math.max(1,Math.min(21,f)):Math.max(0,Math.min(20,f)),n.toString=function(){return t+""},n}function e(t,e){var i=n((t=js(t),t.type="f",t)),r=3*Math.max(-8,Math.min(8,Math.floor(Rs(e)/3))),o=Math.pow(10,-r),u=Bs[8+r/3];return function(t){return i(o*t)+u}}var i=t.grouping&&t.thousands?Fs(t.grouping,t.thousands):re,r=t.currency,o=t.decimal;return{format:n,formatPrefix:e}};!function(t){Xs=Vs(t),Is=Xs.format,$s=Xs.formatPrefix,Xs}({decimal:".",thousands:",",grouping:[3],currency:["$",""]});var Zs=function(t){return Math.max(0,-Rs(Math.abs(t)))},Ws=function(t,n){return Math.max(0,3*Math.max(-8,Math.min(8,Math.floor(Rs(n)/3)))-Rs(Math.abs(t)))},Js=function(t,n){return t=Math.abs(t),n=Math.abs(n)-t,Math.max(0,Rs(n)-Rs(t))+1},Gs=function(){return new oe};oe.prototype={constructor:oe,reset:function(){this.s=this.t=0},add:function(t){ue(Qs,t,this.t),ue(this,Qs.s,this.s),this.s?this.t+=Qs.t:this.s=Qs.t},valueOf:function(){return this.s}};var Qs=new oe,Ks=1e-6,th=Math.PI,nh=th/2,eh=th/4,ih=2*th,rh=th/180,oh=Math.abs,uh=Math.atan,ah=Math.atan2,sh=Math.cos,hh=(Math.ceil,Math.exp),ch=Math.log,lh=(Math.pow,Math.sin),fh=(Math.sign,Math.sqrt),_h=Math.tan;Gs(),Gs(),Gs();_e.invert=_e;var ph=function(){var t,n=[];return{point:function(n,e){t.push([n,e])},lineStart:function(){n.push(t=[])},lineEnd:he,rejoin:function(){n.length>1&&n.push(n.pop().concat(n.shift()))},result:function(){var e=n;return n=[],t=null,e}}},dh=function(t,n){return oh(t[0]-n[0])=0;--o)r.point((c=h[o])[0],c[1]);else i(f.x,f.p.x,-1,r);f=f.p}f=f.o,h=f.z,_=!_}while(!f.v);r.lineEnd()}}},vh=(Gs(),Gs(),Gs(),1/0),gh=-vh;ye.prototype={_radius:4.5,pointRadius:function(t){return this._radius=t,this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._context.closePath(),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._context.moveTo(t,n),this._point=1;break;case 1:this._context.lineTo(t,n);break;default:this._context.moveTo(t+this._radius,n),this._context.arc(t,n,this._radius,0,ih)}},result:he};Gs();ve.prototype={_circle:ge(4.5),pointRadius:function(t){return this._circle=ge(t),this},polygonStart:function(){this._line=0},polygonEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){0===this._line&&this._string.push("Z"),this._point=NaN},point:function(t,n){switch(this._point){case 0:this._string.push("M",t,",",n),this._point=1;break;case 1:this._string.push("L",t,",",n);break;default:this._string.push("M",t,",",n,this._circle)}},result:function(){if(this._string.length){var t=this._string.join("");return this._string=[],t}}};var xh=Gs(),mh=function(t,n){var e=n[0],i=n[1],r=[lh(e),-sh(e),0],o=0,u=0;xh.reset();for(var a=0,s=t.length;a=0?1:-1,N=M*b,k=N>th,T=p*m;if(xh.add(ah(T*M*lh(N),d*w+T*sh(N))),o+=k?b+M*ih:b,k^f>=e^g>=e){var C=le(ce(l),ce(v));fe(C);var S=le(r,C);fe(S);var A=(k^b>=0?-1:1)*se(S[2]);(i>A||i===A&&(C[0]||C[1]))&&(u+=k^b>=0?1:-1)}}return(o<-Ks||o0){for(m||(o.polygonStart(),m=!0),o.lineStart(),t=0;t1&&2&r&&u.push(u.pop().concat(u.shift())),p.push(u.filter(xe))}var _,p,d,y=n(o),v=r.invert(i[0],i[1]),g=ph(),x=n(g),m=!1,w={point:u,lineStart:s,lineEnd:h,polygonStart:function(){w.point=c,w.lineStart=l,w.lineEnd=f,p=[],_=[]},polygonEnd:function(){w.point=u,w.lineStart=s,w.lineEnd=h,p=vo(p);var t=mh(_,v);p.length?(m||(o.polygonStart(),m=!0),yh(p,me,t,e,o)):t&&(m||(o.polygonStart(),m=!0),o.lineStart(),e(null,null,1,o),o.lineEnd()),m&&(o.polygonEnd(),m=!1),p=_=null},sphere:function(){o.polygonStart(),o.lineStart(),e(null,null,1,o),o.lineEnd(),o.polygonEnd()}};return w}};wh(function(){return!0},we,Me,[-th,-nh]);ke.prototype={constructor:ke,point:function(t,n){this.stream.point(t,n)},sphere:function(){this.stream.sphere()},lineStart:function(){this.stream.lineStart()},lineEnd:function(){this.stream.lineEnd()},polygonStart:function(){this.stream.polygonStart()},polygonEnd:function(){this.stream.polygonEnd()}};sh(30*rh),Ne({point:function(t,n){this.stream.point(t*rh,n*rh)}});Te(function(t){return fh(2/(1+t))}).invert=Ce(function(t){return 2*se(t/2)}),Te(function(t){return(t=ae(t))&&t/lh(t)}).invert=Ce(function(t){return t}),Se.invert=function(t,n){return[-n,2*uh(hh(t))-nh]};var bh=function(){return this.eachAfter(Ae)},Mh=function(t){var n,e,i,r,o=this,u=[o];do{for(n=u.reverse(),u=[];o=n.pop();)if(t(o),e=o.children)for(i=0,r=e.length;i=0;--e)r.push(n[e]);return this},kh=function(t){for(var n,e,i,r=this,o=[r],u=[];r=o.pop();)if(u.push(r),n=r.children)for(e=0,i=n.length;e=0;)e+=i[r].value;n.value=e})},Ch=function(t){return this.eachBefore(function(n){n.children&&n.children.sort(t)})},Sh=function(t){for(var n=this,e=Ee(n,t),i=[n];n!==e;)n=n.parent,i.push(n);for(var r=i.length;t!==e;)i.splice(r,0,t),t=t.parent;return i},Ah=function(){for(var t=this,n=[t];t=t.parent;)n.push(t);return n},Eh=function(){var t=[];return this.each(function(n){t.push(n)}),t},Uh=function(){var t=[];return this.eachBefore(function(n){n.children||t.push(n)}),t},Lh=function(){var t=this,n=[];return t.each(function(e){e!==t&&n.push({source:e.parent,target:e})}),n};Fe.prototype=Ue.prototype={constructor:Fe,count:bh,each:Mh,eachAfter:kh,eachBefore:Nh,sum:Th,sort:Ch,path:Sh,ancestors:Ah,descendants:Eh,leaves:Uh,links:Lh,copy:Le};var Ph=function(t,n,e,i,r){for(var o,u=t.children,a=-1,s=u.length,h=t.value&&(i-n)/t.value;++a1?n:1)},e}(Rh);!function t(n){function e(t,e,i,r,o){if((u=t._squarify)&&u.ratio===n)for(var u,a,s,h,c,l=-1,f=u.length,_=t.value;++l1?n:1)},e}(Rh);var Fh=([].slice,{}),Yh=function(t,n){function e(t){var n,e=h.status;if(!e&&$e(h)||e>=200&&e<300||304===e){if(o)try{n=o.call(i,h)}catch(t){return void a.call("error",i,t)}else n=h;a.call("load",i,n)}else a.call("error",i,t)}var i,r,o,u,a=l("beforesend","progress","load","error"),s=Xn(),h=new XMLHttpRequest,c=null,f=null,_=0;if("undefined"==typeof XDomainRequest||"withCredentials"in h||!/^(http(s)?:)?\/\//.test(t)||(h=new XDomainRequest),"onload"in h?h.onload=h.onerror=h.ontimeout=e:h.onreadystatechange=function(t){h.readyState>3&&e(t)},h.onprogress=function(t){a.call("progress",i,t)},i={header:function(t,n){return t=(t+"").toLowerCase(),arguments.length<2?s.get(t):(null==n?s.remove(t):s.set(t,n+""),i)},mimeType:function(t){return arguments.length?(r=null==t?null:t+"",i):r},responseType:function(t){return arguments.length?(u=t,i):u},timeout:function(t){return arguments.length?(_=+t,i):_},user:function(t){return arguments.length<1?c:(c=null==t?null:t+"",i)},password:function(t){return arguments.length<1?f:(f=null==t?null:t+"",i)},response:function(t){return o=t,i},get:function(t,n){return i.send("GET",t,n)},post:function(t,n){return i.send("POST",t,n)},send:function(n,e,o){return h.open(n,t,!0,c,f),null==r||s.has("accept")||s.set("accept",r+",*/*"),h.setRequestHeader&&s.each(function(t,n){h.setRequestHeader(n,t)}),null!=r&&h.overrideMimeType&&h.overrideMimeType(r),null!=u&&(h.responseType=u),_>0&&(h.timeout=_),null==o&&"function"==typeof e&&(o=e,e=null),null!=o&&1===o.length&&(o=Ie(o)),null!=o&&i.on("error",o).on("load",function(t){o(null,t)}),a.call("beforesend",i,h),h.send(null==e?null:e),i},abort:function(){return h.abort(),i},on:function(){var t=a.on.apply(a,arguments);return t===a?i:t}},null!=n){if("function"!=typeof n)throw new Error("invalid callback: "+n);return i.get(n)}return i},qh=function(t,n){return function(e,i){var r=Yh(e).mimeType(t).response(n);if(null!=i){if("function"!=typeof i)throw new Error("invalid callback: "+i);return r.get(i)}return r}};qh("text/html",function(t){return document.createRange().createContextualFragment(t.responseText)}),qh("application/json",function(t){return JSON.parse(t.responseText)}),qh("text/plain",function(t){return t.responseText}),qh("application/xml",function(t){var n=t.responseXML;if(!n)throw new Error("parse error");return n});var Hh=function(t,n){return function(e,i,r){arguments.length<3&&(r=i,i=null);var o=Yh(e).mimeType(t);return o.row=function(t){return arguments.length?o.response(Be(n,i=t)):i},o.row(i),r?o.get(r):o}};Hh("text/csv",ys),Hh("text/tab-separated-values",gs);var zh=Array.prototype,Oh=zh.map,jh=zh.slice,Xh={name:"implicit"},Ih=function(t){return function(){return t}},$h=function(t){return+t},Bh=[0,1],Vh=function(t,n,i){var r,o=t[0],u=t[t.length-1],a=e(o,u,null==n?10:n);switch(i=js(null==i?",f":i),i.type){case"s":var s=Math.max(Math.abs(o),Math.abs(u));return null!=i.precision||isNaN(r=Ws(a,s))||(i.precision=r),$s(i,s);case"":case"e":case"g":case"p":case"r":null!=i.precision||isNaN(r=Js(a,Math.max(Math.abs(o),Math.abs(u))))||(i.precision=r-("e"===i.type));break;case"f":case"%":null!=i.precision||isNaN(r=Zs(a))||(i.precision=r-2*("%"===i.type))}return Is(i)},Zh=new Date,Wh=new Date,Jh=ri(function(){},function(t,n){t.setTime(+t+n)},function(t,n){return n-t});Jh.every=function(t){return t=Math.floor(t),isFinite(t)&&t>0?t>1?ri(function(n){n.setTime(Math.floor(n/t)*t)},function(n,e){n.setTime(+n+e*t)},function(n,e){return(e-n)/t}):Jh:null};var Gh=6e4,Qh=6048e5,Kh=(ri(function(t){t.setTime(1e3*Math.floor(t/1e3))},function(t,n){t.setTime(+t+1e3*n)},function(t,n){return(n-t)/1e3},function(t){return t.getUTCSeconds()}),ri(function(t){t.setTime(Math.floor(t/Gh)*Gh)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getMinutes()}),ri(function(t){var n=t.getTimezoneOffset()*Gh%36e5;n<0&&(n+=36e5),t.setTime(36e5*Math.floor((+t-n)/36e5)+n)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getHours()}),ri(function(t){t.setHours(0,0,0,0)},function(t,n){t.setDate(t.getDate()+n)},function(t,n){return(n-t-(n.getTimezoneOffset()-t.getTimezoneOffset())*Gh)/864e5},function(t){return t.getDate()-1})),tc=oi(0),nc=oi(1),ec=(oi(2),oi(3),oi(4),oi(5),oi(6),ri(function(t){t.setDate(1),t.setHours(0,0,0,0)},function(t,n){t.setMonth(t.getMonth()+n)},function(t,n){return n.getMonth()-t.getMonth()+12*(n.getFullYear()-t.getFullYear())},function(t){return t.getMonth()}),ri(function(t){t.setMonth(0,1),t.setHours(0,0,0,0)},function(t,n){t.setFullYear(t.getFullYear()+n)},function(t,n){return n.getFullYear()-t.getFullYear()},function(t){return t.getFullYear()}));ec.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setFullYear(Math.floor(n.getFullYear()/t)*t),n.setMonth(0,1),n.setHours(0,0,0,0)},function(n,e){n.setFullYear(n.getFullYear()+e*t)}):null};var ic=(ri(function(t){t.setUTCSeconds(0,0)},function(t,n){t.setTime(+t+n*Gh)},function(t,n){return(n-t)/Gh},function(t){return t.getUTCMinutes()}),ri(function(t){t.setUTCMinutes(0,0,0)},function(t,n){t.setTime(+t+36e5*n)},function(t,n){return(n-t)/36e5},function(t){return t.getUTCHours()}),ri(function(t){t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCDate(t.getUTCDate()+n)},function(t,n){return(n-t)/864e5},function(t){return t.getUTCDate()-1})),rc=ui(0),oc=ui(1),uc=(ui(2),ui(3),ui(4),ui(5),ui(6),ri(function(t){t.setUTCDate(1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCMonth(t.getUTCMonth()+n)},function(t,n){return n.getUTCMonth()-t.getUTCMonth()+12*(n.getUTCFullYear()-t.getUTCFullYear())},function(t){return t.getUTCMonth()}),ri(function(t){t.setUTCMonth(0,1),t.setUTCHours(0,0,0,0)},function(t,n){t.setUTCFullYear(t.getUTCFullYear()+n)},function(t,n){return n.getUTCFullYear()-t.getUTCFullYear()},function(t){return t.getUTCFullYear()}));uc.every=function(t){return isFinite(t=Math.floor(t))&&t>0?ri(function(n){n.setUTCFullYear(Math.floor(n.getUTCFullYear()/t)*t),n.setUTCMonth(0,1),n.setUTCHours(0,0,0,0)},function(n,e){n.setUTCFullYear(n.getUTCFullYear()+e*t)}):null};var ac,sc,hc,cc,lc,fc={"-":"",_:" ",0:"0"},_c=/^\s*\d+/,pc=/^%/,dc=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g;!function(t){ac=ci(t),sc=ac.format,hc=ac.parse,cc=ac.utcFormat,lc=ac.utcParse,ac}({dateTime:"%x, %X",date:"%-m/%-d/%Y",time:"%-I:%M:%S %p",periods:["AM","PM"],days:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],shortDays:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],months:["January","February","March","April","May","June","July","August","September","October","November","December"],shortMonths:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]});var yc=(Date.prototype.toISOString||cc("%Y-%m-%dT%H:%M:%S.%LZ"),+new Date("2000-01-01T00:00:00.000Z")||lc("%Y-%m-%dT%H:%M:%S.%LZ"),function(t){return t.match(/.{6}/g).map(function(t){return"#"+t})});yc("1f77b4ff7f0e2ca02cd627289467bd8c564be377c27f7f7fbcbd2217becf"),yc("393b795254a36b6ecf9c9ede6379398ca252b5cf6bcedb9c8c6d31bd9e39e7ba52e7cb94843c39ad494ad6616be7969c7b4173a55194ce6dbdde9ed6"),yc("3182bd6baed69ecae1c6dbefe6550dfd8d3cfdae6bfdd0a231a35474c476a1d99bc7e9c0756bb19e9ac8bcbddcdadaeb636363969696bdbdbdd9d9d9"),yc("1f77b4aec7e8ff7f0effbb782ca02c98df8ad62728ff98969467bdc5b0d58c564bc49c94e377c2f7b6d27f7f7fc7c7c7bcbd22dbdb8d17becf9edae5"),va(Rt(300,.5,0),Rt(-240,.5,1));var vc=(va(Rt(-100,.75,.35),Rt(80,1.5,.8)),va(Rt(260,.75,.35),Rt(80,1.5,.8)),Rt(),function(t){return function(){return t}}),gc=1e-12;Math.PI;ur.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._point=0},lineEnd:function(){(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;default:this._context.lineTo(t,n)}}};var xc=function(t){return new ur(t)},mc=function(){function t(t){var a,s,h,c=t.length,l=!1;for(null==r&&(u=o(h=On())),a=0;a<=c;++a)!(a0)for(var i,r=t[0],o=n[0],u=t[e]-r,a=n[e]-o,s=-1;++s<=e;)i=s/e,this._basis.point(this._beta*t[s]+(1-this._beta)*(r+i*u),this._beta*n[s]+(1-this._beta)*(o+i*a));this._x=this._y=null,this._basis.lineEnd()},point:function(t,n){this._x.push(+t),this._y.push(+n)}},function t(n){function e(t){return 1===n?new fr(t):new _r(t,n)}return e.beta=function(n){return t(+n)},e}(.85),dr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:pr(this,this._x1,this._y1)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2,this._x1=t,this._y1=n;break;case 2:this._point=3;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new dr(t,n)}return e.tension=function(n){return t(+n)},e}(0),yr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new yr(t,n)}return e.tension=function(n){return t(+n)},e}(0),vr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){switch(t=+t,n=+n,this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:pr(this,t,n)}this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return new vr(t,n)}return e.tension=function(n){return t(+n)},e}(0),xr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x2,this._y2);break;case 3:this.point(this._x2,this._y2)}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new xr(t,n):new dr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),mr.prototype={areaStart:wc,areaEnd:wc,lineStart:function(){this._x0=this._x1=this._x2=this._x3=this._x4=this._x5=this._y0=this._y1=this._y2=this._y3=this._y4=this._y5=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){switch(this._point){case 1:this._context.moveTo(this._x3,this._y3),this._context.closePath();break;case 2:this._context.lineTo(this._x3,this._y3),this._context.closePath();break;case 3:this.point(this._x3,this._y3),this.point(this._x4,this._y4),this.point(this._x5,this._y5)}},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1,this._x3=t,this._y3=n;break;case 1:this._point=2,this._context.moveTo(this._x4=t,this._y4=n);break;case 2:this._point=3,this._x5=t,this._y5=n;break;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new mr(t,n):new yr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),wr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._x2=this._y0=this._y1=this._y2=NaN,this._l01_a=this._l12_a=this._l23_a=this._l01_2a=this._l12_2a=this._l23_2a=this._point=0},lineEnd:function(){(this._line||0!==this._line&&3===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){if(t=+t,n=+n,this._point){var e=this._x2-t,i=this._y2-n;this._l23_a=Math.sqrt(this._l23_2a=Math.pow(e*e+i*i,this._alpha))}switch(this._point){case 0:this._point=1;break;case 1:this._point=2;break;case 2:this._point=3,this._line?this._context.lineTo(this._x2,this._y2):this._context.moveTo(this._x2,this._y2);break;case 3:this._point=4;default:gr(this,t,n)}this._l01_a=this._l12_a,this._l12_a=this._l23_a,this._l01_2a=this._l12_2a,this._l12_2a=this._l23_2a,this._x0=this._x1,this._x1=this._x2,this._x2=t,this._y0=this._y1,this._y1=this._y2,this._y2=n}},function t(n){function e(t){return n?new wr(t,n):new vr(t,0)}return e.alpha=function(n){return t(+n)},e}(.5),Tr.prototype={areaStart:function(){this._line=0},areaEnd:function(){this._line=NaN},lineStart:function(){this._x0=this._x1=this._y0=this._y1=this._t0=NaN,this._point=0},lineEnd:function(){switch(this._point){case 2:this._context.lineTo(this._x1,this._y1);break;case 3:kr(this,this._t0,Nr(this,this._t0))}(this._line||0!==this._line&&1===this._point)&&this._context.closePath(),this._line=1-this._line},point:function(t,n){var e=NaN;if(t=+t,n=+n,t!==this._x1||n!==this._y1){switch(this._point){case 0:this._point=1,this._line?this._context.lineTo(t,n):this._context.moveTo(t,n);break;case 1:this._point=2;break;case 2:this._point=3,kr(this,Nr(this,e=Mr(this,t,n)),e);break;default:kr(this,this._t0,e=Mr(this,t,n))}this._x0=this._x1,this._x1=t,this._y0=this._y1,this._y1=n,this._t0=e}}},(Cr.prototype=Object.create(Tr.prototype)).point=function(t,n){Tr.prototype.point.call(this,n,t)},Sr.prototype={moveTo:function(t,n){this._context.moveTo(n,t)},closePath:function(){this._context.closePath()},lineTo:function(t,n){this._context.lineTo(n,t)},bezierCurveTo:function(t,n,e,i,r,o){this._context.bezierCurveTo(n,t,i,e,o,r)}};Array.prototype.slice;Ar.prototype={constructor:Ar,insert:function(t,n){var e,i,r;if(t){if(n.P=t,n.N=t.N,t.N&&(t.N.P=n),t.N=n,t.R){for(t=t.R;t.L;)t=t.L;t.L=n}else t.R=n;e=t}else this._?(t=Pr(this._),n.P=null,n.N=t,t.P=t.L=n,e=t):(n.P=n.N=null,this._=n,e=null);for(n.L=n.R=null,n.U=e,n.C=!0,t=n;e&&e.C;)i=e.U,e===i.L?(r=i.R,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.R&&(Ur(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Lr(this,i))):(r=i.L,r&&r.C?(e.C=r.C=!1,i.C=!0,t=i):(t===e.L&&(Lr(this,e),t=e,e=t.U),e.C=!1,i.C=!0,Ur(this,i))),e=t.U;this._.C=!1},remove:function(t){t.N&&(t.N.P=t.P),t.P&&(t.P.N=t.N),t.N=t.P=null;var n,e,i,r=t.U,o=t.L,u=t.R;if(e=o?u?Pr(u):o:u,r?r.L===t?r.L=e:r.R=e:this._=e,o&&u?(i=e.C,e.C=t.C,e.L=o,o.U=e,e!==u?(r=e.U,e.U=t.U,t=e.R,r.L=t,e.R=u,u.U=e):(e.U=r,r=e,t=e.R)):(i=t.C,t=e),t&&(t.U=r),!i){if(t&&t.C)return void(t.C=!1);do{if(t===this._)break;if(t===r.L){if(n=r.R,n.C&&(n.C=!1,r.C=!0,Ur(this,r),n=r.R),n.L&&n.L.C||n.R&&n.R.C){n.R&&n.R.C||(n.L.C=!1,n.C=!0,Lr(this,n),n=r.R),n.C=r.C,r.C=n.R.C=!1,Ur(this,r),t=this._;break}}else if(n=r.L,n.C&&(n.C=!1,r.C=!0,Lr(this,r),n=r.L),n.L&&n.L.C||n.R&&n.R.C){n.L&&n.L.C||(n.R.C=!1,n.C=!0,Ur(this,n),n=r.L),n.C=r.C,r.C=n.L.C=!1,Lr(this,r),t=this._;break}n.C=!0,t=r,r=r.U}while(!t.C);t&&(t.C=!1)}}};var bc,Mc,Nc,kc,Tc,Cc=[],Sc=[],Ac=1e-6,Ec=1e-12;ro.prototype={constructor:ro,polygons:function(){var t=this.edges;return this.cells.map(function(n){var e=n.halfedges.map(function(e){return jr(n,t[e])});return e.data=n.site.data,e})},triangles:function(){var t=[],n=this.edges;return this.cells.forEach(function(e,i){if(o=(r=e.halfedges).length)for(var r,o,u,a=e.site,s=-1,h=n[r[o-1]],c=h.left===a?h.right:h.left;++s=a)return null;var s=t-r.site[0],h=n-r.site[1],c=s*s+h*h;do{r=o.cells[i=u],u=null,r.halfedges.forEach(function(e){var i=o.edges[e],a=i.left;if(a!==r.site&&a||(a=i.right)){var s=t-a[0],h=n-a[1],l=s*s+h*h;l - {d.value} - {d.data.label} + {d.value} + {d.data.label} ); } diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 34166491..50970789 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -102,8 +102,8 @@ export default class VerticalBarChart extends Component { .attr('text-anchor', 'middle') .attr('x', 100) .attr('y', 100) - .attr('stroke', '#ffffff') - .attr('stroke-width', '1px') + .attr('stroke-width', '0px') + .attr('fill', '#ffffff') .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) .attr('y', d => this.y(d.value) + 15) .text(d => d.value); @@ -115,16 +115,17 @@ export default class VerticalBarChart extends Component { */ render() { const { width } = this.state.dimensions; - const translate = `translate(${margin.left}, ${margin.top})`; + const height = width * ASPECT; + this._renderGraph(this.props); return ( { this.setState({ dimensions }); }}> -
    +
    { this.x ? - this.svg = ref} width={width} height={width * ASPECT} transform={translate}> + this.svg = ref} width={width} height={height} transform={translate}> : null }
    From 23c4da55de36acc71348ef7bc9af809fae7bb25e Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 17 Mar 2017 14:04:07 +0000 Subject: [PATCH 27/53] Make ship summary numbers react to pips/fuel/cargo/etc --- src/app/components/Movement.jsx | 8 ++-- src/app/components/OutfittingSubpages.jsx | 2 +- src/app/components/ShipSummaryTable.jsx | 46 +++++++++++------------ src/app/pages/OutfittingPage.jsx | 3 +- src/app/shipyard/Calculations.js | 32 ++++++++-------- src/app/shipyard/Ship.js | 28 ++------------ src/less/shippicker.less | 1 - 7 files changed, 48 insertions(+), 72 deletions(-) diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index 6f84a052..df993c65 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -58,13 +58,13 @@ export default class Movement extends TranslatedComponent { // Speed - {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s + {formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s // Pitch - {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s // Roll - {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s // Yaw - {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s + {formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s ); } diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 807bf4f7..e8b33620 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -138,7 +138,7 @@ export default class OutfittingSubpages extends TranslatedComponent { } return ( -
    +
    diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 651f9a9f..a982b1df 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -2,6 +2,7 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import cn from 'classnames'; import { Warning } from './SvgIcons'; +import * as Calc from '../shipyard/Calculations'; /** * Ship Summary Table / Stats @@ -9,7 +10,13 @@ import { Warning } from './SvgIcons'; export default class ShipSummaryTable extends TranslatedComponent { static propTypes = { - ship: React.PropTypes.object.isRequired + ship: React.PropTypes.object.isRequired, + 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, + marker: React.PropTypes.string.isRequired, }; /** @@ -17,7 +24,7 @@ export default class ShipSummaryTable extends TranslatedComponent { * @return {React.Component} Summary table */ render() { - let ship = this.props.ship; + const { ship, fuel, eng, cargo, boost } = this.props; let { language, tooltip, termtip } = this.context; let translate = language.translate; let u = language.units; @@ -49,8 +56,7 @@ export default class ShipSummaryTable extends TranslatedComponent { - - + @@ -58,36 +64,28 @@ export default class ShipSummaryTable extends TranslatedComponent { - - - - - - + + - - + + - - - - - - - - - - - - + + + + + + + + diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 7ad4b5d4..eb501fb1 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -375,6 +375,7 @@ export default class OutfittingPage extends Page { // 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()}`; + const shipSummaryMarker = `${ship.toString()}:${eng}:${fuel}:${cargo}`; return (
    @@ -410,7 +411,7 @@ export default class OutfittingPage extends Page {
    {/* Main tables */} - + diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 7e74913b..732328cc 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -9,33 +9,33 @@ import Module from './Module'; * @return {number} Distance in Light Years */ export function jumpRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; } /** - * Calculate the fastest (total) range based on mass and a specific FSD, and all fuel available + * Calculate the total jump range based on mass and a specific FSD, and all fuel available * * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {number} fuel The total fuel available * @return {number} Distance in Light Years */ -export function fastestRange(mass, fsd, fuel) { - let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; - let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; - let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps - let jumps = Math.floor(fuel / fsdMaxFuelPerJump); - mass += fuelRemaining; - // Going backwards, start with the last jump using the remaining fuel - let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0; - // For each max fuel jump, calculate the max jump range based on fuel mass left in the tank - for (let j = 0; j < jumps; j++) { - mass += fsd.maxfuel; - fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; +export function totalJumpRange(mass, fsd, fuel) { + const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; + + let fuelRemaining = fuel; + let totalRange = 0; + while (fuelRemaining > 0) { + const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump); + totalRange += this.jumpRange(mass, fsd, fuelForThisJump); + // Mass is reduced + mass -= fuelForThisJump; + fuelRemaining -= fuelForThisJump; } - return fastestRange; + return totalRange; }; /** diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 9a591fdc..e36cba54 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -138,17 +138,6 @@ export default class Ship { this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost } - /** - * Calculate hypothetical jump range using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Jump range in Light Years - */ - calcJumpRangeWith(fuel, cargo) { - return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); - } - /** * Calculate the hypothetical laden jump range based on a potential change in mass, fuel, or FSD * @param {Number} massDelta Optional - Change in laden mass (mass + cargo + fuel) @@ -173,17 +162,6 @@ export default class Ship { return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); } - /** - * Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {Number} fuel Fuel available in tons - * @param {Number} cargo Cargo in tons - * @return {Number} Total/Cumulative Jump range in Light Years - */ - calcFastestRangeWith(fuel, cargo) { - return Calc.fastestRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel); - } - /** * Calculate the hypothetical top speeds at cargo and fuel tonnage * @param {Number} fuel Fuel available in tons @@ -1398,9 +1376,9 @@ export default class Ship { this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank this.ladenRange = this.calcLadenRange(); // Includes full tank and caro - this.unladenFastestRange = Calc.fastestRange(unladenMass, fsd, fuelCapacity); - this.ladenFastestRange = Calc.fastestRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity); - this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel); + this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity); + this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity); + this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump()); return this; } diff --git a/src/less/shippicker.less b/src/less/shippicker.less index 8c7658c1..982bf5ad 100755 --- a/src/less/shippicker.less +++ b/src/less/shippicker.less @@ -11,7 +11,6 @@ .menu { position: relative; - z-index: 1; cursor: default; &.r { From 369d882354ad257aaa0169087fc0003b33bd7b26 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Fri, 17 Mar 2017 18:10:07 +0000 Subject: [PATCH 28/53] Tidy-ups --- src/app/components/DamageDealt.jsx | 3 - src/app/components/Defence.jsx | 275 +------------------ src/app/components/EngineProfile.jsx | 33 +-- src/app/components/FSDProfile.jsx | 33 +-- src/app/components/JumpRange.jsx | 2 - src/app/components/LineChart.jsx | 131 +++++----- src/app/components/Movement.jsx | 1 - src/app/components/OutfittingSubpages.jsx | 25 +- src/app/components/PieChart.jsx | 5 + src/app/components/WeaponDamageChart.jsx | 250 ++++++++++++++++++ src/app/pages/OutfittingPage.jsx | 33 +-- src/app/shipyard/Calculations.js | 304 ++++++++++++++++++++++ src/app/shipyard/Constants.js | 1 + src/less/charts.less | 3 + 14 files changed, 695 insertions(+), 404 deletions(-) create mode 100644 src/app/components/WeaponDamageChart.jsx diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx index c9bd714e..d1d0fbe6 100644 --- a/src/app/components/DamageDealt.jsx +++ b/src/app/components/DamageDealt.jsx @@ -52,7 +52,6 @@ export function weaponComparator(translate, propComparator, desc) { export default class DamageDealt extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired }; @@ -556,7 +555,6 @@ export default class DamageDealt extends TranslatedComponent {

    {translate('sustained dps against standard shields')}

    {translate('sustained dps against standard armour')}

    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 @@ -371,13 +124,13 @@ export default class Defence extends TranslatedComponent { 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') }); + shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') }); + shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); + 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}
    ); + shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); + shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); + 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)}
    ); @@ -413,12 +166,12 @@ export default class Defence extends TranslatedComponent { } 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') }); + armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); + 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)}
    ); + armourTooltipDetails.push(
    {translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
    ); + armourTooltipDetails.push(
    {translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
    ); const armourAbsoluteTooltipDetails = []; armourAbsoluteTooltipDetails.push(
    {translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}
    ); @@ -480,7 +233,7 @@ export default class Defence extends TranslatedComponent {

    {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('PHRASE_TIME_TO_LOSE_ARMOUR')}
    {armourdamage.totalsdps == 0 ? translate('ever') : 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)}

    diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index fef2a5f5..2faff719 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -15,7 +15,6 @@ import * as Calc from '../shipyard/Calculations'; export default class EngineProfile extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, cargo: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, eng: React.PropTypes.number.isRequired, @@ -87,24 +86,20 @@ export default class EngineProfile extends TranslatedComponent { // 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')}

    - -
    + ); } } diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index 2db23b1d..c5fea602 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -15,7 +15,6 @@ import * as Calc from '../shipyard/Calculations'; export default class FSDProfile extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, cargo: React.PropTypes.number.isRequired, fuel: React.PropTypes.number.isRequired, marker: React.PropTypes.string.isRequired @@ -85,24 +84,20 @@ export default class FSDProfile extends TranslatedComponent { const code = ship.name + ship.toString() + '.' + fuel; return ( - -

    {translate('fsd profile')}

    - -
    + ); } } diff --git a/src/app/components/JumpRange.jsx b/src/app/components/JumpRange.jsx index e8929a58..7fc28c36 100644 --- a/src/app/components/JumpRange.jsx +++ b/src/app/components/JumpRange.jsx @@ -15,7 +15,6 @@ import * as Calc from '../shipyard/Calculations'; export default class JumpRange extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, - chartWidth: React.PropTypes.number.isRequired, code: React.PropTypes.string.isRequired }; @@ -91,7 +90,6 @@ export default class JumpRange extends TranslatedComponent {

    {translate('jump range')}

    0.60), + flip = (xPos / width > 0.60), tipWidth = 0, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; @@ -110,19 +115,21 @@ export default class LineChart extends TranslatedComponent { /** * Update dimensions based on properties and scale - * @param {Object} props React Component properties + * @param {Object} props React Component properties * @param {number} scale size ratio / scale + * @returns {Object} calculated dimensions */ _updateDimensions(props, scale) { - let { width, xMax, xMin, yMin, yMax } = props; - let innerWidth = width - MARGIN.left - MARGIN.right; - let outerHeight = Math.round(width * 0.8 * scale); - let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; + const { xMax, xMin, yMin, yMax } = props; + const { width, height } = this.state.dimensions; + const innerWidth = width - MARGIN.left - MARGIN.right; + const outerHeight = Math.round(width * 2 / 3); // TODO make this an aspect property + const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility - this.setState({ innerWidth, outerHeight, innerHeight }); + return { innerWidth, outerHeight, innerHeight }; } /** @@ -183,7 +190,7 @@ export default class LineChart extends TranslatedComponent { for (let i = 0, l = series ? series.length : 1; i < l; i++) { const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); - detailElems.push(); + detailElems.push(); markerElems.push(); } @@ -196,7 +203,6 @@ export default class LineChart extends TranslatedComponent { * Update dimensions and series data based on props and context. */ componentWillMount() { - this._updateDimensions(this.props, this.context.sizeRatio); this._updateSeries(this.props, this.state); } @@ -206,14 +212,7 @@ export default class LineChart extends TranslatedComponent { * @param {Object} nextContext Incoming/Next conext */ componentWillReceiveProps(nextProps, nextContext) { - let { func, xMin, xMax, yMin, yMax, width } = nextProps; - let props = this.props; - - let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func; - - if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) { - this._updateDimensions(nextProps, nextContext.sizeRatio); - } + const props = this.props; if (props.code != nextProps.code) { this._updateSeries(nextProps, this.state); @@ -225,53 +224,57 @@ export default class LineChart extends TranslatedComponent { * @return {React.Component} Chart SVG */ render() { - if (!this.props.width) { - return null; - } - - let { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; - let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; - let line = this.line; - let lines = seriesLines.map((line, i) => ).reverse(); + const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio); + const { width, height } = this.state.dimensions; + const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; + const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + const line = this.line; + const lines = seriesLines.map((line, i) => ).reverse(); const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; const xmark = xMark ? : ''; - return - - {xmark} - {lines} - d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> - - {xLabel} - ({xUnit}) - - - d3.select(elem).call(this.yAxis)}> - - {yLabel} - { yUnit && ({yUnit}) } - - - this.tipContainer = d3.select(g)} style={{ display: 'none' }}> - - {detailElems} - - this.markersContainer = d3.select(g)} style={{ display: 'none' }}> - {markerElems} - - - - ; + return ( + { this.setState({ dimensions }); }}> +
    + + + {xmark} + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + ({xUnit}) + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + { yUnit && ({yUnit}) } + + + this.tipContainer = d3.select(g)} style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + + + +
    +
    + ); } } diff --git a/src/app/components/Movement.jsx b/src/app/components/Movement.jsx index df993c65..e7432969 100644 --- a/src/app/components/Movement.jsx +++ b/src/app/components/Movement.jsx @@ -33,7 +33,6 @@ export default class Movement extends TranslatedComponent { return ( -

    {translate('movement profile')}

    // Axes diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index e8b33620..6a760833 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -11,6 +11,7 @@ import EngineProfile from './EngineProfile'; import FSDProfile from './FSDProfile'; import Movement from './Movement'; import Defence from './Defence'; +import WeaponDamageChart from './WeaponDamageChart'; /** * Outfitting subpages @@ -21,7 +22,6 @@ export default class OutfittingSubpages extends TranslatedComponent { 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, @@ -73,25 +73,40 @@ export default class OutfittingSubpages extends TranslatedComponent { * @return {React.Component} Tab contents */ _profilesTab() { - const { ship, code, chartWidth, cargo, fuel, eng, boost } = this.props; + const { ship, opponent, cargo, fuel, eng, boost, engagementRange } = this.props; + const { translate } = this.context.language; 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()}`; + const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}`; return
    - +

    {translate('engine profile')}

    +
    - +

    {translate('fsd profile')}

    +
    +

    {translate('movement profile')}

    + +
    +

    {translate('damage to opponent\'s shields')}

    + +
    + +
    +

    {translate('damage to opponent\'s hull')}

    + +
    ; } @@ -114,7 +129,7 @@ export default class OutfittingSubpages extends TranslatedComponent { _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}`; + const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; return
    diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index 62f811f6..98d25800 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -43,6 +43,11 @@ export default class PieChart extends Component { * @returns {Object} the SVG for the slice */ sliceGenerator(d, i) { + if (!d || d.value == 0) { + // Ignore 0 values + return null; + } + const { width, height } = this.state.dimensions; const { data } = this.props; diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx new file mode 100644 index 00000000..90121e5b --- /dev/null +++ b/src/app/components/WeaponDamageChart.jsx @@ -0,0 +1,250 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import LineChart from '../components/LineChart'; +import Slider from '../components/Slider'; +import * as Calc from '../shipyard/Calculations'; +import Module from '../shipyard/Module'; + +const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777']; + +/** + * Weapon damage chart + */ +export default class WeaponDamageChart extends TranslatedComponent { + static propTypes = { + ship: React.PropTypes.object.isRequired, + opponent: React.PropTypes.object.isRequired, + hull: React.PropTypes.bool.isRequired, + engagementRange: React.PropTypes.number.isRequired, + marker: React.PropTypes.string.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + const { ship, opponent, hull } = this.props; + + const maxRange = this._calcMaxRange(ship); + // We take whichever is the higher for shields and hull to ensure same Y axis for both + const maxDps = Math.max(this._calcMaxSDps(ship, opponent, true), this._calcMaxSDps(ship, opponent, false)); + + this.state = { + maxRange, + maxDps + }; + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + const weaponNames = this._weaponNames(this.props.ship, this.context); + this.setState({ weaponNames, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, this.props.hull) }); + } + + /** + * Set the updated weapons state if our ship changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + * @return {boolean} Returns true if the component should be rerendered + */ + componentWillReceiveProps(nextProps, nextContext) { + if (nextProps.marker != this.props.marker) { + const weaponNames = this._weaponNames(nextProps.ship, nextContext); + const maxRange = this._calcMaxRange(nextProps.ship); + // We take whichever is the higher for shields and hull to ensure same Y axis for both + const maxDps = Math.max(this._calcMaxSDps(nextProps.ship, nextProps.opponent, true), this._calcMaxSDps(nextProps.ship, nextProps.opponent, false)); + this.setState({ weaponNames, + maxRange, + maxDps, + calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, nextProps.hull) + }); + } + return true; + } + + /** + * Calculate the maximum range of a ship's weapons + * @param {Object} ship The ship + * @returns {int} The maximum range, in metres + */ + _calcMaxRange(ship) { + let maxRange = 1000; // Minimum + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const thisRange = ship.hardpoints[i].m.getRange(); + if (thisRange > maxRange) { + maxRange = thisRange; + } + } + } + + return maxRange; + } + + /** + * Calculate the maximum sustained single-weapon DPS for this ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {bool} hull True if against hull + * @return {number} The maximum sustained single-weapon DPS + */ + _calcMaxSDps(ship, opponent, hull) { + // Additional information to allow effectiveness calculations + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(opponent, ship, 4, 0); + + let maxSDps = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + const thisSDps = this._calcWeaponSDps(ship, m, opponent, hull, shield, armour, 0); + if (thisSDps > maxSDps) { + maxSDps = thisSDps; + } + } + } + return maxSDps; + } + + /** + * Obtain the weapon names for this ship + * @param {Object} ship The ship + * @param {Object} context The context + * @return {array} The weapon names + */ + _weaponNames(ship, context) { + const translate = context.language.translate; + let names = []; + let num = 1; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp); + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id) { + engineering += ', ' + translate(m.blueprint.special.name); + } + } + if (engineering) { + name = name + ' (' + engineering + ')'; + } + names.push(name); + } + } + return names; + } + + /** + * Calculate the per-weapon sustained DPS for this ship against another ship at a given range + * @param {Object} ship The ship + * @param {Object} weaponNames The names of the weapons for which to calculate DPS + * @param {Object} opponent The target + * @param {bool} hull true if to calculate against hull, false if to calculate against shields + * @param {Object} engagementRange The engagement range + * @return {array} The array of weapon DPS + */ + _calcSDps(ship, weaponNames, opponent, hull, engagementRange) { + // Additional information to allow effectiveness calculations + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(opponent, ship, 4, engagementRange); + + let results = {}; + let weaponNum = 0; + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + results[weaponNames[weaponNum++]] = this._calcWeaponSDps(ship, m, opponent, hull, shield, armour, engagementRange); + } + } + return results; + } + + /** + * Calculate the sustained DPS for a particular weapon for this ship against another ship at a given range + * @param {Object} ship The ship that will deal the damage + * @param {Object} m The weapon that will deal the damage + * @param {Object} opponent The ship against which damage will be dealt + * @param {boolean} hull True if hitting hull + * @param {Object} shield Shield defence metrics + * @param {Object} armour Armour defence metrics + * @param {Object} engagementRange The engagement range + * @return {object} Returns the sustained DPS for the weapon + */ + _calcWeaponSDps(ship, m, opponent, hull, shield, armour, engagementRange) { + let falloff = 1; + if (m.getFalloff()) { + // Calculate the falloff % due to range + if (engagementRange > m.getRange()) { + // Weapon is out of range + falloff = 0; + } else { + const falloffPoint = m.getFalloff(); + if (engagementRange > falloffPoint) { + const falloffRange = m.getRange() - falloffPoint; + // Assuming straight-line falloff + falloff = 1 - (engagementRange - falloffPoint) / falloffRange; + } + } + } + + let effectiveness = 0; + if (m.getDamageDist().E) { + effectiveness += m.getDamageDist().E * (hull ? armour.explosive.total : shield.explosive.total); + } + if (m.getDamageDist().K) { + effectiveness += m.getDamageDist().K * (hull ? armour.kinetic.total : shield.kinetic.total); + } + if (m.getDamageDist().T) { + effectiveness += m.getDamageDist().T * (hull ? armour.thermal.total : shield.thermal.total); + } + if (m.getDamageDist().A) { + effectiveness += m.getDamageDist().A * (hull ? armour.absolute.total : shield.absolute.total); + } + + // Return the final effective SDPS + return (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps()) * falloff * effectiveness; + } + + /** + * Render damage dealt + * @return {React.Component} contents + */ + render() { + const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; + const { formats, translate, units } = language; + const { maxRange } = this.state; + const { ship, opponent } = this.props; + + const sortOrder = this._sortOrder; + const onCollapseExpand = this._onCollapseExpand; + + const code = `${ship.toString()}:${opponent.toString()}`; + + return ( + + + + ); + } +} diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index eb501fb1..ad6e773a 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -14,9 +14,6 @@ import StandardSlotSection from '../components/StandardSlotSection'; import HardpointsSlotSection from '../components/HardpointsSlotSection'; import InternalSlotSection from '../components/InternalSlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection'; -import OffenceSummary from '../components/OffenceSummary'; -import DefenceSummary from '../components/DefenceSummary'; -import MovementSummary from '../components/MovementSummary'; import Pips from '../components/Pips'; import Boost from '../components/Boost'; import Fuel from '../components/Fuel'; @@ -92,7 +89,6 @@ export default class OutfittingPage extends Page { title: this._getTitle(buildName), costTab: Persist.getCostTab() || 'costs', buildName, - thirdChartWidth: 400, newBuildName: buildName, shipId, ship, @@ -268,20 +264,6 @@ export default class OutfittingPage extends Page { Router.replace(outfitURL(shipId, code, buildName)); } - /** - * Update dimenions from rendered DOM - */ - _updateDimensions() { - let elem = findDOMNode(this.refs.chartThird); - - if (elem) { - this.setState({ - thirdChartWidth: findDOMNode(this.refs.chartThird).offsetWidth, - halfChartWidth: findDOMNode(this.refs.chartThird).offsetWidth * 3 / 2 - }); - } - } - /** * Update state based on context changes * @param {Object} nextProps Incoming/Next properties @@ -297,22 +279,14 @@ export default class OutfittingPage extends Page { * Add listeners when about to mount */ componentWillMount() { - this.resizeListener = this.context.onWindowResize(this._updateDimensions); document.addEventListener('keydown', this._keyDown); } - /** - * Trigger DOM updates on mount - */ - componentDidMount() { - this._updateDimensions(); - } - /** * Remove listeners on unmount */ componentWillUnmount() { - this.resizeListener.remove(); + document.removeEventListener('keydown', this._keyDown); } /** @@ -360,7 +334,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, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = state, + { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = state, hide = tooltip.bind(null, null), menu = this.props.currentMenu, shipUpdated = this._shipUpdated, @@ -446,6 +420,7 @@ export default class OutfittingPage extends Page {
    + {/* Tabbed subpages */}
    - ); } } diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 732328cc..8c7a5368 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -310,3 +310,307 @@ export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boos return result; } + /** + * Calculate defence metrics + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The pips to SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Defence metrics + */ +export function defenceMetrics(ship, opponent, sys, engagementrange) { + const sysResistance = this.sysResistance(sys); + const maxSysResistance = this.sysResistance(4); + + // Obtain the opponent's sustained DPS on us for later damage calculations + const { shieldsdps, armoursdps } = this._sustainedDps(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 = this.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.sysRechargeRate(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 + */ +export function sysResistance(sys) { + return Math.pow(sys,0.85) * 0.6 / Math.pow(4,0.85); +} + +/** + * 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 + */ +export function sysRechargeRate(pd, sys) { + return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1); +} + +/** + * 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 + */ +export function _sustainedDps(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 }; +} + diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index a273c578..4e2d8012 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -47,6 +47,7 @@ export const ModuleGroupToName = { pcm: 'First Class Passenger Cabin', pcq: 'Luxury Passenger Cabin', cc: 'Collector Limpet Controller', + ss: 'Surface Scanner', // Hard Points bl: 'Beam Laser', diff --git a/src/less/charts.less b/src/less/charts.less index 556e15d8..22006184 100755 --- a/src/less/charts.less +++ b/src/less/charts.less @@ -44,6 +44,9 @@ svg { .label, .text-tip { text-transform: capitalize; + } + + .x { fill: @fg; } From eb83969015838c76caa34c2616f8a3b8f969c202 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 18 Mar 2017 13:42:32 +0000 Subject: [PATCH 29/53] Break out metric calculations --- src/app/components/Defence.jsx | 61 +-- src/app/components/Offence.jsx | 477 ++++------------------ src/app/components/OutfittingSubpages.jsx | 5 +- src/app/components/WeaponDamageChart.jsx | 23 +- src/app/shipyard/Calculations.js | 96 ++++- 5 files changed, 181 insertions(+), 481 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 39b4a22f..0a1ba4fe 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -1,8 +1,6 @@ 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'; @@ -48,64 +46,7 @@ export default class Defence extends TranslatedComponent { } /** - * 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 }; - } - - /** - * Render shields + * Render defence * @return {React.Component} contents */ render() { diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 86307353..506b6fcd 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -1,13 +1,16 @@ 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 + * Offence information consists of four panels: + * - textual information (time to drain cap, time to take down shields etc.) + * - breakdown of damage sources (pie chart) + * - comparison of shield resistances (table chart) + * - effective sustained DPS of weapons (bar chart) */ export default class Offence extends TranslatedComponent { static propTypes = { @@ -25,7 +28,7 @@ export default class Offence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, shielddamage, armourdamage } = this._calcMetrics(props.ship, props.opponent, props.sys, props.engagementrange); + const { shield, armour, shielddamage, armourdamage } = Calc.offenceMetrics(props.ship, props.opponent, props.eng, props.engagementrange); this.state = { shield, armour, shielddamage, armourdamage }; } @@ -36,419 +39,116 @@ export default class Offence extends TranslatedComponent { */ 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); + const { shield, armour, shielddamage, armourdamage } = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.engagementrange); this.setState({ shield, armour, shielddamage, armourdamage }); - return true; } + 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 + * Render offence * @return {React.Component} contents */ render() { - const { ship, sys } = this.props; + const { ship, wep } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { shield, armour, shielddamage, armourdamage } = this.state; +// 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') }); +// const shieldSourcesData = []; +// const effectiveShieldData = []; +// const shieldDamageTakenData = []; +// const shieldTooltipDetails = []; +// const shieldAbsoluteTooltipDetails = []; +// const shieldExplosiveTooltipDetails = []; +// const shieldKineticTooltipDetails = []; +// const shieldThermalTooltipDetails = []; +// let maxEffectiveShield = 0; +// if (shield.total) { +// shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') }); +// shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); +// 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}
    ); +// shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); +// shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); +// 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)}
    ); +// 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)}
    ); +// 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)}
    ); +// 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)}
    ); +// 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') }); +// 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') }); +// 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); - } +// 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 armourSourcesData = []; +// armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); +// 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 armourTooltipDetails = []; +// armourTooltipDetails.push(
    {translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
    ); +// 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 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 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 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 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 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') }); +// 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')}

    @@ -475,7 +175,7 @@ export default class Offence extends TranslatedComponent {

    {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('PHRASE_TIME_TO_LOSE_ARMOUR')}
    {armourdamage.totalsdps == 0 ? translate('ever') : 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)}

    @@ -493,6 +193,7 @@ export default class Offence extends TranslatedComponent {

    {translate('effective armour')}

    + */}
    ); } } diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 6a760833..18f7ac47 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -10,6 +10,7 @@ import CostSection from './CostSection'; import EngineProfile from './EngineProfile'; import FSDProfile from './FSDProfile'; import Movement from './Movement'; +import Offence from './Offence'; import Defence from './Defence'; import WeaponDamageChart from './WeaponDamageChart'; @@ -117,8 +118,10 @@ export default class OutfittingSubpages extends TranslatedComponent { _offenceTab() { const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; + return
    -

    Offence goes here

    +
    ; } diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx index 90121e5b..990225aa 100644 --- a/src/app/components/WeaponDamageChart.jsx +++ b/src/app/components/WeaponDamageChart.jsx @@ -100,13 +100,12 @@ export default class WeaponDamageChart extends TranslatedComponent { */ _calcMaxSDps(ship, opponent, hull) { // Additional information to allow effectiveness calculations - const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(opponent, ship, 4, 0); - + const defence = hull ? Calc.armourMetrics(opponent) : Calc.shieldMetrics(opponent, 4); let maxSDps = 0; for (let i = 0; i < ship.hardpoints.length; i++) { if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { const m = ship.hardpoints[i].m; - const thisSDps = this._calcWeaponSDps(ship, m, opponent, hull, shield, armour, 0); + const thisSDps = this._calcWeaponSDps(ship, m, opponent, defence, 0); if (thisSDps > maxSDps) { maxSDps = thisSDps; } @@ -156,14 +155,14 @@ export default class WeaponDamageChart extends TranslatedComponent { */ _calcSDps(ship, weaponNames, opponent, hull, engagementRange) { // Additional information to allow effectiveness calculations - const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(opponent, ship, 4, engagementRange); + const defence = hull ? Calc.armourMetrics(opponent) : Calc.shieldMetrics(opponent, 4); let results = {}; let weaponNum = 0; for (let i = 0; i < ship.hardpoints.length; i++) { if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { const m = ship.hardpoints[i].m; - results[weaponNames[weaponNum++]] = this._calcWeaponSDps(ship, m, opponent, hull, shield, armour, engagementRange); + results[weaponNames[weaponNum++]] = this._calcWeaponSDps(ship, m, opponent, defence, engagementRange); } } return results; @@ -174,13 +173,11 @@ export default class WeaponDamageChart extends TranslatedComponent { * @param {Object} ship The ship that will deal the damage * @param {Object} m The weapon that will deal the damage * @param {Object} opponent The ship against which damage will be dealt - * @param {boolean} hull True if hitting hull - * @param {Object} shield Shield defence metrics - * @param {Object} armour Armour defence metrics + * @param {Object} defence defence metrics (either shield or hull) * @param {Object} engagementRange The engagement range * @return {object} Returns the sustained DPS for the weapon */ - _calcWeaponSDps(ship, m, opponent, hull, shield, armour, engagementRange) { + _calcWeaponSDps(ship, m, opponent, defence, engagementRange) { let falloff = 1; if (m.getFalloff()) { // Calculate the falloff % due to range @@ -199,16 +196,16 @@ export default class WeaponDamageChart extends TranslatedComponent { let effectiveness = 0; if (m.getDamageDist().E) { - effectiveness += m.getDamageDist().E * (hull ? armour.explosive.total : shield.explosive.total); + effectiveness += m.getDamageDist().E * defence.explosive.total; } if (m.getDamageDist().K) { - effectiveness += m.getDamageDist().K * (hull ? armour.kinetic.total : shield.kinetic.total); + effectiveness += m.getDamageDist().K * defence.kinetic.total; } if (m.getDamageDist().T) { - effectiveness += m.getDamageDist().T * (hull ? armour.thermal.total : shield.thermal.total); + effectiveness += m.getDamageDist().T * defence.thermal.total; } if (m.getDamageDist().A) { - effectiveness += m.getDamageDist().A * (hull ? armour.absolute.total : shield.absolute.total); + effectiveness += m.getDamageDist().A * defence.absolute.total; } // Return the final effective SDPS diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 8c7a5368..b27be63e 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -310,23 +310,18 @@ export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boos return result; } - /** - * Calculate defence metrics - * @param {Object} ship The ship - * @param {Object} opponent The opponent ship - * @param {int} sys The pips to SYS - * @param {int} engagementrange The range between the ship and opponent - * @returns {Object} Defence metrics - */ -export function defenceMetrics(ship, opponent, sys, engagementrange) { +/** + * Calculate shield metrics + * @param {Object} ship The ship + * @param {int} sys The pips to SYS + * @returns {Object} Shield metrics + */ +export function shieldMetrics(ship, sys) { const sysResistance = this.sysResistance(sys); const maxSysResistance = this.sysResistance(4); - // Obtain the opponent's sustained DPS on us for later damage calculations - const { shieldsdps, armoursdps } = this._sustainedDps(opponent, ship, engagementrange); - - let shielddamage = {}; let shield = {}; + const shieldGeneratorSlot = ship.findInternalByGroup('sg'); if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) { const shieldGenerator = shieldGeneratorSlot.m; @@ -451,14 +446,17 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { 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; } + return shield; +} + +/** + * Calculate armour metrics + * @param {Object} ship The ship + * @returns {Object} Armour metrics + */ +export function armourMetrics(ship) { // Armour from bulkheads const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost()); let armourReinforcement = 0; @@ -527,6 +525,35 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg }; + return armour; +} + +/** + * Calculate defence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} sys The pips to SYS + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Defence metrics + */ +export function defenceMetrics(ship, opponent, sys, engagementrange) { + // Obtain the shield metrics + const shield = this.shieldMetrics(ship, sys); + + // Obtain the armour metrics + const armour = this.armourMetrics(ship); + + // Obtain the opponent's sustained DPS on us + const { shieldsdps, armoursdps } = this._sustainedDps(opponent, ship, engagementrange); + + const shielddamage = shield.generatorStrength ? { + absolutesdps: shieldsdps.absolute *= shield.absolute.total, + explosivesdps: shieldsdps.explosive *= shield.explosive.total, + kineticsdps: shieldsdps.kinetic *= shield.kinetic.total, + thermalsdps: shieldsdps.thermal *= shield.thermal.total, + totalsdps: shielddamage.absolutesdps + shielddamage.explosivesdps + shielddamage.kineticsdps + shielddamage.thermalsdps + } : {} ; + const armourdamage = { absolutesdps: armoursdps.absolute *= armour.absolute.total, explosivesdps: armoursdps.explosive *= armour.explosive.total, @@ -538,6 +565,37 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { return { shield, armour, shielddamage, armourdamage }; } +/** + * Calculate offence metrics for a ship + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {int} wep The pips to WEP + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Offence metrics + */ +export function offenceMetrics(ship, opponent, wep, engagementrange) { + + // Per-weapon and total damage to armour + const armourdamage = {}; + + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, 4); + const opponentArmour = this.armourMetrics(opponent); + + // Per-weapon and total damage to shields + const shielddamage = opponentShields.generatorStrength ? { + absolute: { + weapon1: 10, + weapon2: 10, + weapon3: 10, + weapon4: 10, + total: 40 + } + } : {}; + + return { shielddamage, armourdamage }; +} + /** * Calculate the resistance provided by SYS pips * @param {integer} sys the value of the SYS pips From 49a076fd9eb3a9913a030f9d28b97129cac70b72 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 18 Mar 2017 23:46:46 +0000 Subject: [PATCH 30/53] Updates --- src/app/components/Defence.jsx | 26 +-- src/app/components/LineChart.jsx | 2 +- src/app/components/Offence.jsx | 174 ++++++++++++------ src/app/components/VerticalBarChart.jsx | 12 +- src/app/components/WeaponDamageChart.jsx | 97 +++------- src/app/shipyard/Calculations.js | 220 ++++++++++++++++++----- 6 files changed, 342 insertions(+), 189 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 0a1ba4fe..902e1bea 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -69,9 +69,9 @@ export default class Defence extends TranslatedComponent { shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') }); - shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); - shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); - shieldTooltipDetails.push(
    {translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}
    ); + if (shield.generator > 0) shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); + if (shield.boosters > 0) shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); + if (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)}
    ); @@ -98,10 +98,10 @@ export default class Defence extends TranslatedComponent { 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') }); + shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldAbsoluteTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldExplosiveTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldKineticTooltipDetails }); + shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldThermalTooltipDetails }); maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max); } @@ -111,8 +111,8 @@ export default class Defence extends TranslatedComponent { armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); const armourTooltipDetails = []; - armourTooltipDetails.push(
    {translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
    ); - armourTooltipDetails.push(
    {translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}
    ); + 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)}
    ); @@ -141,10 +141,10 @@ export default class Defence extends TranslatedComponent { 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') }); + armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourAbsoluteTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourExplosiveTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourKineticTooltipDetails }); + armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourThermalTooltipDetails }); return ( diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 696cb82b..24edc448 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -84,7 +84,7 @@ export default class LineChart extends TranslatedComponent { y0 = func(x0), tips = this.tipContainer, yTotal = 0, - flip = (xPos / width > 0.60), + flip = (xPos / width > 0.50), tipWidth = 0, tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 506b6fcd..45a1841e 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -2,8 +2,45 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import * as Calc from '../shipyard/Calculations'; import PieChart from './PieChart'; +import { nameComparator } from '../utils/SlotFunctions'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; import VerticalBarChart from './VerticalBarChart'; +/** + * Generates an internationalization friendly weapon comparator that will + * sort by specified property (if provided) then by name/group, class, rating + * @param {function} translate Translation function + * @param {function} propComparator Optional property comparator + * @param {boolean} desc Use descending order + * @return {function} Comparator function for names + */ +export function weaponComparator(translate, propComparator, desc) { + return (a, b) => { + if (!desc) { // Flip A and B if ascending order + let t = a; + a = b; + b = t; + } + + // If a property comparator is provided use it first + let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b); + + if (diff) { + return diff; + } + + // Property matches so sort by name / group, then class, rating + if (a.name === b.name && a.grp === b.grp) { + if(a.class == b.class) { + return a.rating > b.rating ? 1 : -1; + } + return a.class - b.class; + } + + return nameComparator(translate, a, b); + }; +} + /** * Offence information * Offence information consists of four panels: @@ -28,8 +65,14 @@ export default class Offence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, shielddamage, armourdamage } = Calc.offenceMetrics(props.ship, props.opponent, props.eng, props.engagementrange); - this.state = { shield, armour, shielddamage, armourdamage }; + this._sort = this._sort.bind(this); + + const damage = Calc.offenceMetrics(props.ship, props.opponent, props.eng, props.engagementrange); + this.state = { + predicate: 'n', + desc: true, + damage + }; } /** @@ -39,12 +82,51 @@ export default class Offence extends TranslatedComponent { */ componentWillReceiveProps(nextProps) { if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { - const { shield, armour, shielddamage, armourdamage } = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.engagementrange); - this.setState({ shield, armour, shielddamage, armourdamage }); + const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.engagementrange); + this.setState({ damage }); } return true; } + /** + * Set the sort order and sort + * @param {string} predicate Sort predicate + */ + _sortOrder(predicate) { + let desc = this.state.desc; + + if (predicate == this.state.predicate) { + desc = !desc; + } else { + desc = true; + } + + this._sort(this.props.ship, predicate, desc); + this.setState({ predicate, desc }); + } + + /** + * Sorts the weapon list + * @param {Ship} ship Ship instance + * @param {string} predicate Sort predicate + * @param {Boolean} desc Sort order descending + */ + _sort(ship, predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); + + switch (predicate) { + case 'n': comp = comp(null, desc); break; + case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break; + case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break; + case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break; + case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break; + case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break; + case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break; + } + + this.state.damage.sort(comp); + } + /** * Render offence * @return {React.Component} contents @@ -53,8 +135,26 @@ export default class Offence extends TranslatedComponent { const { ship, wep } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; -// const { shield, armour, shielddamage, armourdamage } = this.state; + const { damage } = this.state; + const sortOrder = this._sortOrder; + const rows = []; + for (let i = 0; i < damage.length; i++) { + const weapon = damage[i]; + rows.push(
    + + + + + + ); + } // const shieldSourcesData = []; // const effectiveShieldData = []; // const shieldDamageTakenData = []; @@ -148,52 +248,24 @@ export default class Offence extends TranslatedComponent { 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('ever') : 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')}

    - -
    - */} +
    {translate('mass')} {translate('cargo')} {translate('fuel')}{translate('jump range')}{translate('fastest range')}{translate('jump range')} {translate('crew')} {translate('MLF')}
    {translate('hull')} {translate('unladen')} {translate('laden')}{translate('max')}{translate('full tank')}{translate('laden')}{translate('jumps')}{translate('unladen')}{translate('laden')}{translate('single')}{translate('total')}
    { ship.canThrust() ? {int(ship.topSpeed)} {u['m/s']} : 0 }{ ship.canBoost() ? {int(ship.topBoost)} {u['m/s']} : 0 }{ ship.canThrust() ? {int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']} : 0 }{ ship.canBoost() ? {int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']} : 0 } {f1(ship.totalDps)} {f1(ship.totalEps)} {ship.timeToDrain === Infinity ? '∞' : time(ship.timeToDrain)} {f1(ship.totalHps)} {int(ship.hardness)} {int(ship.armour)}{int(ship.shield)} {u.MJ}{ship.hullMass} {u.T}{int(ship.unladenMass)} {u.T}{int(ship.ladenMass)} {u.T}{round(ship.cargoCapacity)} {u.T}{round(ship.fuelCapacity)} {u.T}{f2(ship.unladenRange)} {u.LY}{f2(ship.fullTankRange)} {u.LY}{f2(ship.ladenRange)} {u.LY}{int(ship.maxJumpCount)}{f2(ship.unladenFastestRange)} {u.LY}{f2(ship.ladenFastestRange)} {u.LY}{int(ship.shield)}{u.MJ}{ship.hullMass}{u.T}{int(ship.unladenMass)}{u.T}{int(ship.ladenMass)}{u.T}{round(ship.cargoCapacity)}{u.T}{round(ship.fuelCapacity)}{u.T}{f2(Calc.jumpRange(ship.unladenMass + fuel + cargo, ship.standard[2].m, fuel))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + fuel + cargo, ship.standard[2].m, fuel))}{u.LY} {ship.crew} {ship.masslock}
    + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + {weapon.engineering ? ' (' + weapon.engineering + ')' : null } + {formats.f1(weapon.sdpsShields)}{formats.pct1(weapon.effectivenessShields)}{formats.f1(weapon.sdpsArmour)}{formats.pct1(weapon.effectivenessArmour)}
    + + + + + + + + + + + + + + + {rows} + +
    {translate('weapon')}{translate('opponent shields')}{translate('opponent armour')}
    {translate('effective sdps')}{translate('effectiveness')}{translate('effective sdps')}{translate('effectiveness')}
    ); } } diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 50970789..e6fa37cc 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import Measure from 'react-measure'; import * as d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#519032', '#D5420D']; const LABEL_COLOUR = '#FFFFFF'; @@ -16,7 +17,7 @@ const merge = function(one, two) { /** * A vertical bar chart */ -export default class VerticalBarChart extends Component { +export default class VerticalBarChart extends TranslatedComponent { static propTypes = { data : React.PropTypes.array.isRequired, @@ -45,6 +46,7 @@ export default class VerticalBarChart extends Component { */ _renderGraph(props) { let { width, height } = this.state.dimensions; + const { tooltip, termtip } = this.context; width = width - margin.left - margin.right, height = width * ASPECT - margin.top - margin.bottom; @@ -104,9 +106,9 @@ export default class VerticalBarChart extends Component { .attr('y', 100) .attr('stroke-width', '0px') .attr('fill', '#ffffff') - .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) - .attr('y', d => this.y(d.value) + 15) - .text(d => d.value); + .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) + .attr('y', d => this.y(d.value) + 15) + .text(d => d.value); } /** @@ -125,7 +127,7 @@ export default class VerticalBarChart extends Component { { this.setState({ dimensions }); }}>
    { this.x ? - this.svg = ref} width={width} height={height} transform={translate}> + this.svg = ref} width={width} height={height}> : null }
    diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx index 990225aa..dfb7f237 100644 --- a/src/app/components/WeaponDamageChart.jsx +++ b/src/app/components/WeaponDamageChart.jsx @@ -30,17 +30,6 @@ export default class WeaponDamageChart extends TranslatedComponent { */ constructor(props, context) { super(props); - - const { ship, opponent, hull } = this.props; - - const maxRange = this._calcMaxRange(ship); - // We take whichever is the higher for shields and hull to ensure same Y axis for both - const maxDps = Math.max(this._calcMaxSDps(ship, opponent, true), this._calcMaxSDps(ship, opponent, false)); - - this.state = { - maxRange, - maxDps - }; } /** @@ -48,7 +37,12 @@ export default class WeaponDamageChart extends TranslatedComponent { */ componentWillMount() { const weaponNames = this._weaponNames(this.props.ship, this.context); - this.setState({ weaponNames, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, this.props.hull) }); + const opponentShields = Calc.shieldMetrics(this.props.opponent, 4); + const opponentArmour = Calc.armourMetrics(this.props.opponent); + const maxRange = this._calcMaxRange(this.props.ship); + const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour); + + this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) }); } /** @@ -60,13 +54,16 @@ export default class WeaponDamageChart extends TranslatedComponent { componentWillReceiveProps(nextProps, nextContext) { if (nextProps.marker != this.props.marker) { const weaponNames = this._weaponNames(nextProps.ship, nextContext); + const opponentShields = Calc.shieldMetrics(nextProps.opponent, 4); + const opponentArmour = Calc.armourMetrics(nextProps.opponent); const maxRange = this._calcMaxRange(nextProps.ship); - // We take whichever is the higher for shields and hull to ensure same Y axis for both - const maxDps = Math.max(this._calcMaxSDps(nextProps.ship, nextProps.opponent, true), this._calcMaxSDps(nextProps.ship, nextProps.opponent, false)); + const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour); this.setState({ weaponNames, + opponentShields, + opponentArmour, maxRange, maxDps, - calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, nextProps.hull) + calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull) }); } return true; @@ -93,19 +90,21 @@ export default class WeaponDamageChart extends TranslatedComponent { /** * Calculate the maximum sustained single-weapon DPS for this ship - * @param {Object} ship The ship - * @param {Object} opponent The opponent ship - * @param {bool} hull True if against hull - * @return {number} The maximum sustained single-weapon DPS + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour + * @return {number} The maximum sustained single-weapon DPS */ - _calcMaxSDps(ship, opponent, hull) { + _calcMaxSDps(ship, opponent, opponentShields, opponentArmour) { // Additional information to allow effectiveness calculations - const defence = hull ? Calc.armourMetrics(opponent) : Calc.shieldMetrics(opponent, 4); let maxSDps = 0; for (let i = 0; i < ship.hardpoints.length; i++) { if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { const m = ship.hardpoints[i].m; - const thisSDps = this._calcWeaponSDps(ship, m, opponent, defence, 0); + + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0); + const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; if (thisSDps > maxSDps) { maxSDps = thisSDps; } @@ -149,69 +148,25 @@ export default class WeaponDamageChart extends TranslatedComponent { * @param {Object} ship The ship * @param {Object} weaponNames The names of the weapons for which to calculate DPS * @param {Object} opponent The target + * @param {Object} opponentShields The opponent's shields + * @param {Object} opponentArmour The opponent's armour * @param {bool} hull true if to calculate against hull, false if to calculate against shields * @param {Object} engagementRange The engagement range * @return {array} The array of weapon DPS */ - _calcSDps(ship, weaponNames, opponent, hull, engagementRange) { - // Additional information to allow effectiveness calculations - const defence = hull ? Calc.armourMetrics(opponent) : Calc.shieldMetrics(opponent, 4); - + _calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) { let results = {}; let weaponNum = 0; for (let i = 0; i < ship.hardpoints.length; i++) { if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { const m = ship.hardpoints[i].m; - results[weaponNames[weaponNum++]] = this._calcWeaponSDps(ship, m, opponent, defence, engagementRange); + const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange); + results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; } } return results; } - /** - * Calculate the sustained DPS for a particular weapon for this ship against another ship at a given range - * @param {Object} ship The ship that will deal the damage - * @param {Object} m The weapon that will deal the damage - * @param {Object} opponent The ship against which damage will be dealt - * @param {Object} defence defence metrics (either shield or hull) - * @param {Object} engagementRange The engagement range - * @return {object} Returns the sustained DPS for the weapon - */ - _calcWeaponSDps(ship, m, opponent, defence, engagementRange) { - let falloff = 1; - if (m.getFalloff()) { - // Calculate the falloff % due to range - if (engagementRange > m.getRange()) { - // Weapon is out of range - falloff = 0; - } else { - const falloffPoint = m.getFalloff(); - if (engagementRange > falloffPoint) { - const falloffRange = m.getRange() - falloffPoint; - // Assuming straight-line falloff - falloff = 1 - (engagementRange - falloffPoint) / falloffRange; - } - } - } - - let effectiveness = 0; - if (m.getDamageDist().E) { - effectiveness += m.getDamageDist().E * defence.explosive.total; - } - if (m.getDamageDist().K) { - effectiveness += m.getDamageDist().K * defence.kinetic.total; - } - if (m.getDamageDist().T) { - effectiveness += m.getDamageDist().T * defence.thermal.total; - } - if (m.getDamageDist().A) { - effectiveness += m.getDamageDist().A * defence.absolute.total; - } - - // Return the final effective SDPS - return (m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps()) * falloff * effectiveness; - } - /** * Render damage dealt * @return {React.Component} contents diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index b27be63e..549fcf2a 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -544,23 +544,23 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { const armour = this.armourMetrics(ship); // Obtain the opponent's sustained DPS on us - const { shieldsdps, armoursdps } = this._sustainedDps(opponent, ship, engagementrange); + const sustainedDps = this.sustainedDps(opponent, ship, sys, engagementrange); - const shielddamage = shield.generatorStrength ? { - absolutesdps: shieldsdps.absolute *= shield.absolute.total, - explosivesdps: shieldsdps.explosive *= shield.explosive.total, - kineticsdps: shieldsdps.kinetic *= shield.kinetic.total, - thermalsdps: shieldsdps.thermal *= shield.thermal.total, - totalsdps: shielddamage.absolutesdps + shielddamage.explosivesdps + shielddamage.kineticsdps + shielddamage.thermalsdps - } : {} ; + const shielddamage = shield.generator ? { + absolutesdps: sustainedDps.shieldsdps.absolute, + explosivesdps: sustainedDps.shieldsdps.explosive, + kineticsdps: sustainedDps.shieldsdps.kinetic, + thermalsdps: sustainedDps.shieldsdps.thermal, + totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal + } : {}; 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 + absolutesdps: sustainedDps.armoursdps.absolute, + explosivesdps: sustainedDps.armoursdps.explosive, + kineticsdps: sustainedDps.armoursdps.kinetic, + thermalsdps: sustainedDps.armoursdps.thermal, + totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal }; - armourdamage.totalsdps = armourdamage.absolutesdps + armourdamage.explosivesdps + armourdamage.kineticsdps + armourdamage.thermalsdps; return { shield, armour, shielddamage, armourdamage }; } @@ -571,19 +571,48 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { * @param {Object} opponent The opponent ship * @param {int} wep The pips to WEP * @param {int} engagementrange The range between the ship and opponent - * @returns {Object} Offence metrics + * @returns {array} Offence metrics */ export function offenceMetrics(ship, opponent, wep, engagementrange) { - - // Per-weapon and total damage to armour - const armourdamage = {}; + // Per-weapon and total damage + const damage = []; // Obtain the opponent's shield and armour metrics const opponentShields = this.shieldMetrics(opponent, 4); const opponentArmour = this.armourMetrics(opponent); // Per-weapon and total damage to shields - const shielddamage = opponentShields.generatorStrength ? { + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) { + const m = ship.hardpoints[i].m; + + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + let engineering; + if (m.blueprint && m.blueprint.name) { + engineering = m.blueprint.name + ' ' + 'grade' + ' ' + m.blueprint.grade; + if (m.blueprint.special && m.blueprint.special.id >= 0) { + engineering += ', ' + m.blueprint.special.name; + } + } + + const weaponSustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + damage.push({ + id: i, + mount: m.mount, + name: m.name || m.grp, + classRating, + engineering, + sdpsShields: weaponSustainedDps.damage.shields.total, + sdpsArmour: weaponSustainedDps.damage.armour.total, + effectivenessShields: weaponSustainedDps.effectiveness.shields.total, + effectivenessArmour: weaponSustainedDps.effectiveness.armour.total + }); + } + } + + return damage; + + const shielddamage = opponentShields.generator ? { absolute: { weapon1: 10, weapon2: 10, @@ -593,7 +622,7 @@ export function offenceMetrics(ship, opponent, wep, engagementrange) { } } : {}; - return { shielddamage, armourdamage }; + return damage; } /** @@ -616,13 +645,31 @@ export function sysRechargeRate(pd, sys) { } /** - * Calculate the sustained DPS for a ship at a given range, excluding resistances + * Calculate the sustained DPS for a ship against an opponent at a given range * @param {Object} ship The ship * @param {Object} opponent The opponent ship + * @param {number} sys Pips to opponent's SYS * @param {int} engagementrange The range between the ship and opponent * @returns {Object} Sustained DPS for shield and armour */ -export function _sustainedDps(ship, opponent, engagementrange) { +export function sustainedDps(ship, opponent, sys, engagementrange) { + // Obtain the opponent's shield and armour metrics + const opponentShields = this.shieldMetrics(opponent, sys); + const opponentArmour = this.armourMetrics(opponent); + + return this._sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange); +} + +/** + * Calculate the sustained DPS for a ship against an opponent at a given range + * @param {Object} ship The ship + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange) { const shieldsdps = { absolute: 0, explosive: 0, @@ -640,35 +687,112 @@ export function _sustainedDps(ship, opponent, engagementrange) { 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; - } + const sustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange); + shieldsdps.absolute += sustainedDps.damage.shields.absolute; + shieldsdps.explosive += sustainedDps.damage.shields.explosive; + shieldsdps.kinetic += sustainedDps.damage.shields.kinetic; + shieldsdps.thermal += sustainedDps.damage.shields.thermal; + armoursdps.absolute += sustainedDps.damage.armour.absolute; + armoursdps.explosive += sustainedDps.damage.armour.explosive; + armoursdps.kinetic += sustainedDps.damage.armour.kinetic; + armoursdps.thermal += sustainedDps.damage.armour.thermal; } } + return { shieldsdps, armoursdps }; } +/** + * Calculate the sustained DPS for a weapon at a given range + * @param {Object} m The weapon + * @param {Object} opponent The opponent ship + * @param {Object} opponentShields The opponent's shield resistances + * @param {Object} opponentArmour The opponent's armour resistances + * @param {int} engagementrange The range between the ship and opponent + * @returns {Object} Sustained DPS for shield and armour + */ +export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { + const weapon = { + damage: { + shields: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + armour: { + absolute: 0, + explosive: 0, + kinetic: 0, + thermal: 0, + total: 0 + }, + }, + effectiveness: { + shields: { + range: 1, + sys: opponentShields.absolute.sys, + resistance: 1 + }, + armour: { + range: 1, + hardness: 1, + resistance: 1 + } + } + }; + + // 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; + const dropoff = 1 - Math.min((engagementrange - falloff) / dropoffRange, 1); + weapon.effectiveness.shields.range = weapon.effectiveness.armour.range = dropoff; + sDps *= dropoff; + } + + // Piercing/hardness modifier (for armour only) + const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness; + weapon.effectiveness.armour.hardness = armourMultiple; + + // Break out the damage according to type + let shieldsResistance = 0; + let armourResistance = 0; + if (m.getDamageDist().A) { + weapon.damage.shields.absolute += sDps * m.getDamageDist().A * opponentShields.absolute.total; + weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total; + shieldsResistance += m.getDamageDist().A * opponentShields.absolute.generator * opponentShields.absolute.boosters; + armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement; + } + if (m.getDamageDist().E) { + weapon.damage.shields.explosive += sDps * m.getDamageDist().E * opponentShields.explosive.total; + weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total; + shieldsResistance += m.getDamageDist().E * opponentShields.explosive.generator * opponentShields.explosive.boosters; + armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement; + } + if (m.getDamageDist().K) { + weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * opponentShields.kinetic.total; + weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total; + shieldsResistance += m.getDamageDist().K * opponentShields.kinetic.generator * opponentShields.kinetic.boosters; + armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement; + } + if (m.getDamageDist().T) { + weapon.damage.shields.thermal += sDps * m.getDamageDist().T * opponentShields.thermal.total; + weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total; + shieldsResistance += m.getDamageDist().T * opponentShields.thermal.generator * opponentShields.thermal.boosters; + armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement; + } + weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal; + weapon.damage.armour.total = weapon.damage.armour.absolute + weapon.damage.armour.explosive + weapon.damage.armour.kinetic + weapon.damage.armour.thermal; + + weapon.effectiveness.shields.resistance *= shieldsResistance; + weapon.effectiveness.armour.resistance *= armourResistance; + + weapon.effectiveness.shields.total = weapon.effectiveness.shields.range * weapon.effectiveness.shields.sys * weapon.effectiveness.shields.resistance; + weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness; + return weapon; +} From fcb8980a388d92723bcad78c34249544c216268a Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 08:41:36 +0000 Subject: [PATCH 31/53] Tidy-ups --- src/app/components/EngagementRange.jsx | 2 +- src/app/components/Offence.jsx | 164 ++++++++----------------- src/app/i18n/en.js | 5 + src/app/pages/OutfittingPage.jsx | 2 +- src/app/shipyard/Calculations.js | 6 +- src/less/app.less | 1 + src/less/offence.less | 14 +++ 7 files changed, 75 insertions(+), 119 deletions(-) create mode 100755 src/less/offence.less diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx index ef851b01..724a197f 100644 --- a/src/app/components/EngagementRange.jsx +++ b/src/app/components/EngagementRange.jsx @@ -27,7 +27,7 @@ export default class Range extends TranslatedComponent { this.state = { maxRange, - rangeLevel: 0.5, + rangeLevel: 1000 / maxRange, }; } diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 45a1841e..65327fe1 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -81,7 +81,7 @@ export default class Offence extends TranslatedComponent { * @return {boolean} Returns true if the component should be rerendered */ componentWillReceiveProps(nextProps) { - if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { + if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) { const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.engagementrange); this.setState({ damage }); } @@ -101,27 +101,24 @@ export default class Offence extends TranslatedComponent { desc = true; } - this._sort(this.props.ship, predicate, desc); + this._sort(predicate, desc); this.setState({ predicate, desc }); } /** * Sorts the weapon list - * @param {Ship} ship Ship instance * @param {string} predicate Sort predicate * @param {Boolean} desc Sort order descending */ - _sort(ship, predicate, desc) { + _sort(predicate, desc) { let comp = weaponComparator.bind(null, this.context.language.translate); switch (predicate) { case 'n': comp = comp(null, desc); break; - case 'edpss': comp = comp((a, b) => a.effectiveDpsShields - b.effectiveDpsShields, desc); break; - case 'esdpss': comp = comp((a, b) => a.effectiveSDpsShields - b.effectiveSDpsShields, desc); break; - case 'es': comp = comp((a, b) => a.effectivenessShields - b.effectivenessShields, desc); break; - case 'edpsh': comp = comp((a, b) => a.effectiveDpsHull - b.effectiveDpsHull, desc); break; - case 'esdpsh': comp = comp((a, b) => a.effectiveSDpsHull - b.effectiveSDpsHull, desc); break; - case 'eh': comp = comp((a, b) => a.effectivenessHull - b.effectivenessHull, desc); break; + case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break; + case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break; + case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break; + case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break; } this.state.damage.sort(comp); @@ -141,104 +138,43 @@ export default class Offence extends TranslatedComponent { const rows = []; for (let i = 0; i < damage.length; i++) { const weapon = damage[i]; - rows.push( - - {weapon.mount == 'F' ? : null} - {weapon.mount == 'G' ? : null} - {weapon.mount == 'T' ? : null} - {weapon.classRating} {translate(weapon.name)} - {weapon.engineering ? ' (' + weapon.engineering + ')' : null } - - {formats.f1(weapon.sdpsShields)} - {formats.pct1(weapon.effectivenessShields)} - {formats.f1(weapon.sdpsArmour)} - {formats.pct1(weapon.effectivenessArmour)} - ); + + const effectivenessShieldsTooltipDetails = []; + effectivenessShieldsTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}
    ); + effectivenessShieldsTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}
    ); + effectivenessShieldsTooltipDetails.push(
    {translate('sys') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
    ); + + const effectiveShieldsSDpsTooltipDetails = []; + if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(
    {translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}
    ); + if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(
    {translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}
    ); + if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(
    {translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}
    ); + if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(
    {translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}
    ); + + const effectivenessArmourTooltipDetails = []; + effectivenessArmourTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}
    ); + effectivenessArmourTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}
    ); + effectivenessArmourTooltipDetails.push(
    {translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}
    ); + const effectiveArmourSDpsTooltipDetails = []; + if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(
    {translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}
    ); + if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(
    {translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}
    ); + if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(
    {translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}
    ); + if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(
    {translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}
    ); + + rows.push( + + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + {weapon.engineering ? ' (' + weapon.engineering + ')' : null } + + {formats.f1(weapon.sdps.shields.total)} + {formats.pct1(weapon.effectiveness.shields.total)} + {formats.f1(weapon.sdps.armour.total)} + {formats.pct1(weapon.effectiveness.armour.total)} + ); } -// const shieldSourcesData = []; -// const effectiveShieldData = []; -// const shieldDamageTakenData = []; -// const shieldTooltipDetails = []; -// const shieldAbsoluteTooltipDetails = []; -// const shieldExplosiveTooltipDetails = []; -// const shieldKineticTooltipDetails = []; -// const shieldThermalTooltipDetails = []; -// let maxEffectiveShield = 0; -// if (shield.total) { -// shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') }); -// shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') }); -// shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') }); - -// shieldTooltipDetails.push(
    {translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
    ); -// shieldTooltipDetails.push(
    {translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
    ); -// 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 = []; -// armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') }); -// armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') }); - -// const armourTooltipDetails = []; -// armourTooltipDetails.push(
    {translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}
    ); -// 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') }); @@ -248,24 +184,26 @@ export default class Offence extends TranslatedComponent { return ( +
    - - + + - - - - + + + + {rows}
    {translate('weapon')}{translate('opponent shields')}{translate('opponent armour')}{translate('opponent\`s shields')}{translate('opponent\`s armour')}
    {translate('effective sdps')}{translate('effectiveness')}{translate('effective sdps')}{translate('effectiveness')}{'sdps'}{'eft'}{'sdps'}{'eft'}
    +
    ); } } diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 1f7d8cb5..37683424 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -55,6 +55,11 @@ export const terms = { TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages', TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages', + TT_EFFECTIVE_SDPS_SHIELDS: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_SHIELDS: 'Effectivness compared to hitting a 0-resistance target with 0 pips to SYS at 0m', + TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty', + TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m', + HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', // Other languages fallback to these values diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index ad6e773a..b96c2ef3 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -100,7 +100,7 @@ export default class OutfittingPage extends Page { fuel: ship.fuelCapacity, cargo: 0, boost: false, - engagementRange: 1500, + engagementRange: 1000, opponent: new Ship('anaconda', Ships['anaconda'].properties, Ships['anaconda'].slots).buildWith(Ships['anaconda'].defaults) }; } diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 549fcf2a..597b279d 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -602,10 +602,8 @@ export function offenceMetrics(ship, opponent, wep, engagementrange) { name: m.name || m.grp, classRating, engineering, - sdpsShields: weaponSustainedDps.damage.shields.total, - sdpsArmour: weaponSustainedDps.damage.armour.total, - effectivenessShields: weaponSustainedDps.effectiveness.shields.total, - effectivenessArmour: weaponSustainedDps.effectiveness.armour.total + sdps: weaponSustainedDps.damage, + effectiveness: weaponSustainedDps.effectiveness }); } } diff --git a/src/less/app.less b/src/less/app.less index f1234db6..5c015eff 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -24,6 +24,7 @@ @import 'movement'; @import 'shippicker'; @import 'defence'; +@import 'offence'; html, body { height: 100%; diff --git a/src/less/offence.less b/src/less/offence.less new file mode 100755 index 00000000..39679f27 --- /dev/null +++ b/src/less/offence.less @@ -0,0 +1,14 @@ +#offence { + table { + background-color: @bgBlack; + color: @fg; + margin: 0 auto; + } + + .icon { + stroke: @fg; + stroke-width: 20; + fill: transparent; + } +} + From 0ff95ed1f1a1919cee2fccbfac7005ca29b34d36 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 08:49:38 +0000 Subject: [PATCH 32/53] Shrink movement profile --- src/app/components/OutfittingSubpages.jsx | 2 +- src/less/movement.less | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 18f7ac47..d381ec72 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -96,7 +96,7 @@ export default class OutfittingSubpages extends TranslatedComponent {

    {translate('movement profile')}

    - +
    diff --git a/src/less/movement.less b/src/less/movement.less index 30eb083a..796468f5 100644 --- a/src/less/movement.less +++ b/src/less/movement.less @@ -1,8 +1,8 @@ #movement { svg { - width: 100%; - height: 100%; + width: 75%; + height: 75%; stroke: @primary-disabled; fill: @primary-disabled; From 2736e1df79d75bc93b64e76b9bafe02272682ebc Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 16:16:00 +0000 Subject: [PATCH 33/53] More metrics --- src/app/components/Offence.jsx | 59 +++++++++++++++++++++---- src/app/components/PieChart.jsx | 4 +- src/app/components/ShipSummaryTable.jsx | 6 ++- src/app/i18n/en.js | 2 + src/app/shipyard/Calculations.js | 46 +++++++++++++++---- src/app/shipyard/Ship.js | 38 ---------------- 6 files changed, 97 insertions(+), 58 deletions(-) diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 65327fe1..d3272c53 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -129,16 +129,37 @@ export default class Offence extends TranslatedComponent { * @return {React.Component} contents */ render() { - const { ship, wep } = this.props; + const { ship, opponent, wep, engagementrange } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; const { damage } = this.state; const sortOrder = this._sortOrder; + const opponentShields = Calc.shieldMetrics(opponent, 4); + const opponentArmour = Calc.armourMetrics(opponent); + + let absoluteShieldsSDps = 0; + let explosiveShieldsSDps = 0; + let kineticShieldsSDps = 0; + let thermalShieldsSDps = 0; + let absoluteArmourSDps = 0; + let explosiveArmourSDps = 0; + let kineticArmourSDps = 0; + let thermalArmourSDps = 0; + const rows = []; for (let i = 0; i < damage.length; i++) { const weapon = damage[i]; + absoluteShieldsSDps += weapon.sdps.shields.absolute; + explosiveShieldsSDps += weapon.sdps.shields.explosive; + kineticShieldsSDps += weapon.sdps.shields.kinetic; + thermalShieldsSDps += weapon.sdps.shields.thermal; + absoluteArmourSDps += weapon.sdps.armour.absolute; + explosiveArmourSDps += weapon.sdps.armour.explosive; + kineticArmourSDps += weapon.sdps.armour.kinetic; + thermalArmourSDps += weapon.sdps.armour.thermal; + const effectivenessShieldsTooltipDetails = []; effectivenessShieldsTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}
    ); effectivenessShieldsTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}
    ); @@ -176,16 +197,25 @@ export default class Offence extends TranslatedComponent { ); } -// 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') }); + const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps; + const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps; + + const shieldsSDpsData = []; + shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') }); + shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') }); + shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') }); + shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') }); + + const armourSDpsData = []; + armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') }); + armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') }); + armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') }); + armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') }); return ( -
    - +
    +
    @@ -204,6 +234,19 @@ export default class Offence extends TranslatedComponent {
    {translate('weapon')}
    +
    +

    {translate('shield damage')}

    +

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {totalShieldsSDps == 0 ? translate('never') : formats.time(opponentShields.total / totalShieldsSDps)}

    +

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {totalArmourSDps == 0 ? translate('never') : formats.time(opponentArmour.total / totalArmourSDps)}

    +
    +
    +

    {translate('shield damage sources')}

    + +
    +
    +

    {translate('armour damage sources')}

    + +
    ); } } diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index 98d25800..a6ef2e68 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -56,7 +56,9 @@ export default class PieChart extends Component { const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; // Put the keys in a line with equal spacing - const keyX = -width / 2 + (width / data.length) * (i + 0.5); + const nonZeroItems = data.filter(d => d.value != 0).length; + const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; + const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); const keyTranslate = `translate(${keyX}, ${width * 0.45})`; return ( diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index a982b1df..370cb03b 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -24,7 +24,7 @@ export default class ShipSummaryTable extends TranslatedComponent { * @return {React.Component} Summary table */ render() { - const { ship, fuel, eng, cargo, boost } = this.props; + const { ship, fuel, eng, wep, cargo, boost } = this.props; let { language, tooltip, termtip } = this.context; let translate = language.translate; let u = language.units; @@ -40,6 +40,8 @@ export default class ShipSummaryTable extends TranslatedComponent { sgRecharge = time(ship.calcShieldRecharge()); } + const timeToDrain = Calc.timeToDrainWep(ship, wep); + return
    @@ -74,7 +76,7 @@ export default class ShipSummaryTable extends TranslatedComponent { - + diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 37683424..ad969a1b 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -48,6 +48,8 @@ export const terms = { PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for', PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints', PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules', + PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields', + PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour', TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 597b279d..ea77c8a0 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -710,6 +710,7 @@ export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, e * @returns {Object} Sustained DPS for shield and armour */ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { + const opponentHasShields = opponentShields.generator ? true : false; const weapon = { damage: { shields: { @@ -730,7 +731,7 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour effectiveness: { shields: { range: 1, - sys: opponentShields.absolute.sys, + sys: opponentHasShields ? opponentShields.absolute.sys : 1, resistance: 1 }, armour: { @@ -761,27 +762,27 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour let shieldsResistance = 0; let armourResistance = 0; if (m.getDamageDist().A) { - weapon.damage.shields.absolute += sDps * m.getDamageDist().A * opponentShields.absolute.total; + weapon.damage.shields.absolute += sDps * m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.total : 1); weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total; - shieldsResistance += m.getDamageDist().A * opponentShields.absolute.generator * opponentShields.absolute.boosters; + shieldsResistance += m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.generator * opponentShields.absolute.boosters : 1); armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement; } if (m.getDamageDist().E) { - weapon.damage.shields.explosive += sDps * m.getDamageDist().E * opponentShields.explosive.total; + weapon.damage.shields.explosive += sDps * m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.total : 1); weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total; - shieldsResistance += m.getDamageDist().E * opponentShields.explosive.generator * opponentShields.explosive.boosters; + shieldsResistance += m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.generator * opponentShields.explosive.boosters : 1); armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement; } if (m.getDamageDist().K) { - weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * opponentShields.kinetic.total; + weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.total : 1); weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total; - shieldsResistance += m.getDamageDist().K * opponentShields.kinetic.generator * opponentShields.kinetic.boosters; + shieldsResistance += m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.generator * opponentShields.kinetic.boosters : 1); armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement; } if (m.getDamageDist().T) { - weapon.damage.shields.thermal += sDps * m.getDamageDist().T * opponentShields.thermal.total; + weapon.damage.shields.thermal += sDps * m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.total : 1); weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total; - shieldsResistance += m.getDamageDist().T * opponentShields.thermal.generator * opponentShields.thermal.boosters; + shieldsResistance += m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.generator * opponentShields.thermal.boosters : 1); armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement; } weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal; @@ -794,3 +795,30 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness; return weapon; } + +/** + * 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 + */ +export function timeToDrainWep(ship, wep) { + let totalSEps = 0; + + for (let slotNum in ship.hardpoints) { + const slot = ship.hardpoints[slotNum]; + if (slot.maxClass > 0 && slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { + totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); + } + } + + // Calculate the drain time + const drainPerSecond = totalSEps - ship.standard[4].m.getWeaponsRechargeRate() * wep / 4; + if (drainPerSecond <= 0) { + // Can fire forever + return Infinity; + } else { + const initialCharge = ship.standard[4].m.getWeaponsCapacity(); + return initialCharge / drainPerSecond; + } +} diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index e36cba54..a1c11650 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -453,7 +453,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -522,15 +521,11 @@ export default class Ship { this.recalculateDps(); this.recalculateHps(); this.recalculateEps(); - this.recalculateTtd(); } else if (name === 'explres' || name === 'kinres' || name === 'thermres') { m.setModValue(name, value, sentfromui); // Could be for shields or armour this.recalculateArmour(); this.recalculateShield(); - } else if (name === 'wepcap' || name === 'weprate') { - m.setModValue(name, value, sentfromui); - this.recalculateTtd(); } else if (name === 'engcap') { m.setModValue(name, value, sentfromui); // Might have resulted in a change in boostability @@ -668,7 +663,6 @@ export default class Ship { .recalculateDps() .recalculateEps() .recalculateHps() - .recalculateTtd() .updateMovement(); } @@ -850,7 +844,6 @@ export default class Ship { if (slot.m.getEps()) { this.recalculateEps(); - this.recalculateTtd(); } } } @@ -926,7 +919,6 @@ export default class Ship { } if (epsChanged) { this.recalculateEps(); - this.recalculateTtd(); } if (hpsChanged) { this.recalculateHps(); @@ -934,9 +926,6 @@ export default class Ship { if (powerGeneratedChange) { this.updatePowerGenerated(); } - if (powerDistributorChange) { - this.recalculateTtd(); - } if (powerUsedChange) { this.updatePowerUsed(); } @@ -974,33 +963,6 @@ export default class Ship { return val; } - /** - * Calculate time to drain WEP capacitor - * @return {this} The ship instance (for chaining operations) - */ - recalculateTtd() { - let totalSEps = 0; - - for (let slotNum in this.hardpoints) { - const slot = this.hardpoints[slotNum]; - if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) { - totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps(); - } - } - - // Calculate the drain time - const drainPerSecond = totalSEps - this.standard[4].m.getWeaponsRechargeRate(); - if (drainPerSecond <= 0) { - // Can fire forever - this.timeToDrain = Infinity; - } else { - const initialCharge = this.standard[4].m.getWeaponsCapacity(); - this.timeToDrain = initialCharge / drainPerSecond; - } - - return this; - } - /** * Calculate damage per second and related items for weapons * @return {this} The ship instance (for chaining operations) From 6afd80002c0347f1e82ab9ee287c835b9af1af86 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 17:40:41 +0000 Subject: [PATCH 34/53] Tidy-ups --- src/app/components/Defence.jsx | 6 +++-- src/app/components/Offence.jsx | 12 ++++++++-- src/app/i18n/en.js | 6 +++++ src/app/shipyard/Calculations.js | 39 +++++++++++++++++++++++++++++--- 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 902e1bea..56a1bafc 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -55,6 +55,8 @@ export default class Defence extends TranslatedComponent { const { formats, translate, units } = language; const { shield, armour, shielddamage, armourdamage } = this.state; + const pd = ship.standard[4].m; + const shieldSourcesData = []; const effectiveShieldData = []; const shieldDamageTakenData = []; @@ -153,7 +155,7 @@ export default class Defence extends TranslatedComponent {

    {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_LOSE_SHIELDS')}
    {shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate()))}

    {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)}

    @@ -174,7 +176,7 @@ export default class Defence extends TranslatedComponent {

    {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('ever') : formats.time(armour.total / armourdamage.totalsdps)}

    +

    {translate('PHRASE_TIME_TO_LOSE_ARMOUR')}
    {armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate()))}

    {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)}

    diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index d3272c53..52e0791c 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -135,6 +135,8 @@ export default class Offence extends TranslatedComponent { const { damage } = this.state; const sortOrder = this._sortOrder; + const pd = ship.standard[4].m; + const opponentShields = Calc.shieldMetrics(opponent, 4); const opponentArmour = Calc.armourMetrics(opponent); @@ -147,10 +149,13 @@ export default class Offence extends TranslatedComponent { let kineticArmourSDps = 0; let thermalArmourSDps = 0; + let totalSEps = 0; + const rows = []; for (let i = 0; i < damage.length; i++) { const weapon = damage[i]; + totalSEps += weapon.seps; absoluteShieldsSDps += weapon.sdps.shields.absolute; explosiveShieldsSDps += weapon.sdps.shields.explosive; kineticShieldsSDps += weapon.sdps.shields.kinetic; @@ -212,6 +217,9 @@ export default class Offence extends TranslatedComponent { armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') }); armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') }); + const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + return (
    @@ -236,8 +244,8 @@ export default class Offence extends TranslatedComponent {

    {translate('shield damage')}

    -

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {totalShieldsSDps == 0 ? translate('never') : formats.time(opponentShields.total / totalShieldsSDps)}

    -

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {totalArmourSDps == 0 ? translate('never') : formats.time(opponentArmour.total / totalArmourSDps)}

    +

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}

    +

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}

    {translate('shield damage sources')}

    diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index ad969a1b..cf986c7b 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -44,6 +44,8 @@ export const terms = { PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in', PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy', PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types', + PHRASE_ARMOUR_SOURCES: 'Breakdown of the supply of armour', + PHRASE_EFFECTIVE_ARMOUR: 'Effective armour strength against different damage types', PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types', PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for', PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints', @@ -51,6 +53,10 @@ export const terms = { PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields', PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour', + PHRASE_TIME_TO_REMOVE_SHIELDS: 'Will remove shields in', + TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons', + PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in', + TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons', TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', TT_MODULE_ARMOUR: 'Armour protecting against module damage', diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index ea77c8a0..183ec30e 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -551,7 +551,8 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { explosivesdps: sustainedDps.shieldsdps.explosive, kineticsdps: sustainedDps.shieldsdps.kinetic, thermalsdps: sustainedDps.shieldsdps.thermal, - totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal + totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal, + totalseps: sustainedDps.eps } : {}; const armourdamage = { @@ -559,7 +560,8 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { explosivesdps: sustainedDps.armoursdps.explosive, kineticsdps: sustainedDps.armoursdps.kinetic, thermalsdps: sustainedDps.armoursdps.thermal, - totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal + totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal, + totalseps: sustainedDps.eps }; return { shield, armour, shielddamage, armourdamage }; @@ -603,6 +605,7 @@ export function offenceMetrics(ship, opponent, wep, engagementrange) { classRating, engineering, sdps: weaponSustainedDps.damage, + seps: weaponSustainedDps.eps, effectiveness: weaponSustainedDps.effectiveness }); } @@ -682,6 +685,8 @@ export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, e thermal: 0 }; + let eps = 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; @@ -694,10 +699,11 @@ export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, e armoursdps.explosive += sustainedDps.damage.armour.explosive; armoursdps.kinetic += sustainedDps.damage.armour.kinetic; armoursdps.thermal += sustainedDps.damage.armour.thermal; + eps += sustainedDps.eps; } } - return { shieldsdps, armoursdps }; + return { shieldsdps, armoursdps, eps }; } /** @@ -712,6 +718,7 @@ export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, e export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) { const opponentHasShields = opponentShields.generator ? true : false; const weapon = { + eps: 0, damage: { shields: { absolute: 0, @@ -742,6 +749,9 @@ export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour } }; + // EPS + weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps(); + // Initial sustained DPS let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps(); @@ -822,3 +832,26 @@ export function timeToDrainWep(ship, wep) { return initialCharge / drainPerSecond; } } + +/** + * Calculate the time to deplete an amount of shields or armour + */ +export function timeToDeplete(amount, dps, eps, capacity, recharge) { + const drainPerSecond = eps - recharge; + if (drainPerSecond <= 0) { + // Simple result + return amount / dps; + } else { + // We are draining the capacitor, but can we deplete before we run out + const timeToDrain = capacity / drainPerSecond; + const depletedBeforeDrained = dps * timeToDrain; + if (depletedBeforeDrained >= amount) { + return amount / dps; + } else { + const restToDeplete = amount - depletedBeforeDrained; + // We delete the rest at the reduced rate + const reducedDps = dps * (recharge / eps); + return timeToDrain + (restToDeplete / reducedDps); + } + } +} From cadd699bdfe3f802308896cbe6273ca7b9df377c Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 18:53:22 +0000 Subject: [PATCH 35/53] Manual merge of #90 --- src/app/components/Header.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index ff880f1b..c36660da 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -356,7 +356,7 @@ export default class Header extends TranslatedComponent { let comps = Object.keys(Persist.getComparisons()).sort(); for (let name of comps) { - comparisons.push({name}); + comparisons.push({name}); } } else { comparisons = {translate('none created')}; From 117028875fe4f3912c7dca1cf786a9c0d712d539 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 18:54:38 +0000 Subject: [PATCH 36/53] Name change --- src/app/components/Offence.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 52e0791c..1c823c3c 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -243,7 +243,7 @@ export default class Offence extends TranslatedComponent {
    { ship.canBoost() ? {int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']} : 0 } {f1(ship.totalDps)} {f1(ship.totalEps)}{ship.timeToDrain === Infinity ? '∞' : time(ship.timeToDrain)}{timeToDrain === Infinity ? '∞' : time(timeToDrain)} {f1(ship.totalHps)} {int(ship.hardness)} {int(ship.armour)}
    -

    {translate('shield damage')}

    +

    {translate('damage to opponent')}

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}

    From 86df6f20f6c213620d17e49893a555c8e79a6826 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 20:18:31 +0000 Subject: [PATCH 37/53] Ensure ship picker z index is correct --- src/less/shippicker.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/less/shippicker.less b/src/less/shippicker.less index 982bf5ad..bd5c4c17 100755 --- a/src/less/shippicker.less +++ b/src/less/shippicker.less @@ -61,7 +61,7 @@ background-color: @bgBlack; font-size: 0.9em; overflow-y: auto; - z-index: 0; + z-index: 1; -webkit-overflow-scrolling: touch; max-height: 500px; From e278babee8c78163aa94f96d778ec343ebbe62ca Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sun, 19 Mar 2017 22:34:19 +0000 Subject: [PATCH 38/53] Use correct values for boost/speed when working out boost factor --- src/app/shipyard/Ship.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index a1c11650..ff564bbb 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -181,7 +181,7 @@ export default class Ship { * @return {Number} Speed */ calcSpeed(eng, fuel, cargo, boost) { - return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost); + return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost); } /** From 2f5d123f029f8933b863b7056968ee53e616f3ed Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 20 Mar 2017 07:48:44 +0000 Subject: [PATCH 39/53] Change chart label colour to black; persist select outfitting subpage --- src/app/components/OutfittingSubpages.jsx | 14 ++++++++++---- src/app/components/PieChart.jsx | 4 ++-- src/app/components/VerticalBarChart.jsx | 6 +++--- src/app/stores/Persist.js | 18 ++++++++++++++++++ 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index d381ec72..e6d5f663 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -2,8 +2,7 @@ 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 Persist from '../stores/Persist'; import TranslatedComponent from './TranslatedComponent'; import PowerManagement from './PowerManagement'; import CostSection from './CostSection'; @@ -42,9 +41,12 @@ export default class OutfittingSubpages extends TranslatedComponent { constructor(props) { super(props); this._powerTab = this._powerTab.bind(this); + this._profilesTab = this._profilesTab.bind(this); + this._offenceTab = this._offenceTab.bind(this); + this._defenceTab = this._defenceTab.bind(this); this.state = { - tab: 'power' + tab: Persist.getOutfittingTab() || 'power', }; } @@ -62,6 +64,7 @@ export default class OutfittingSubpages extends TranslatedComponent { */ _powerTab() { let { ship, buildName, code, onChange } = this.props; + Persist.setOutfittingTab('power'); return
    @@ -77,6 +80,7 @@ export default class OutfittingSubpages extends TranslatedComponent { const { ship, opponent, cargo, fuel, eng, boost, engagementRange } = this.props; const { translate } = this.context.language; let realBoost = boost && ship.canBoost(); + Persist.setOutfittingTab('profiles'); const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`; const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`; @@ -117,6 +121,7 @@ export default class OutfittingSubpages extends TranslatedComponent { */ _offenceTab() { const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + Persist.setOutfittingTab('offence'); const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; @@ -131,6 +136,7 @@ export default class OutfittingSubpages extends TranslatedComponent { */ _defenceTab() { const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + Persist.setOutfittingTab('defence'); const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; @@ -144,7 +150,7 @@ export default class OutfittingSubpages extends TranslatedComponent { * @return {React.Component} Contents */ render() { - const tab = this.state.tab || 'power'; + const tab = this.state.tab; const translate = this.context.language.translate; let tabSection; diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index a6ef2e68..c5e5435e 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -2,8 +2,8 @@ import React, { Component } from 'react'; import Measure from 'react-measure'; import * as d3 from 'd3'; -const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#519032', '#D5420D']; -const LABEL_COLOUR = '#FFFFFF'; +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; /** * A pie chart diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index e6fa37cc..617b1b4b 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -3,8 +3,8 @@ import Measure from 'react-measure'; import * as d3 from 'd3'; import TranslatedComponent from './TranslatedComponent'; -const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#519032', '#D5420D']; -const LABEL_COLOUR = '#FFFFFF'; +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; const margin = { top: 10, right: 0, bottom: 0, left: 55 }; @@ -105,7 +105,7 @@ export default class VerticalBarChart extends TranslatedComponent { .attr('x', 100) .attr('y', 100) .attr('stroke-width', '0px') - .attr('fill', '#ffffff') + .attr('fill', LABEL_COLOUR) .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) .attr('y', d => this.y(d.value) + 15) .text(d => d.value); diff --git a/src/app/stores/Persist.js b/src/app/stores/Persist.js index 4ef740f6..e1ea7b82 100644 --- a/src/app/stores/Persist.js +++ b/src/app/stores/Persist.js @@ -5,6 +5,7 @@ const LS_KEY_BUILDS = 'builds'; const LS_KEY_COMPARISONS = 'comparisons'; const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; const LS_KEY_COST_TAB = 'costTab'; +const LS_KEY_OUTFITTING_TAB = 'outfittingTab'; const LS_KEY_INSURANCE = 'insurance'; const LS_KEY_SHIP_DISCOUNT = 'shipDiscount'; const LS_KEY_MOD_DISCOUNT = 'moduleDiscount'; @@ -98,6 +99,7 @@ export class Persist extends EventEmitter { this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {}; this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {}; this.costTab = _getString(LS_KEY_COST_TAB); + this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB); this.state = _get(LS_KEY_STATE); this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.tooltipsEnabled = tips === null ? true : tips; @@ -472,6 +474,22 @@ export class Persist extends EventEmitter { return this.costTab; } + /** + * Persist selected outfitting tab + * @param {string} tabName Cost tab name + */ + setOutfittingTab(tabName) { + this.outfittingTab = tabName; + _put(LS_KEY_OUTFITTING_TAB, tabName); + } + /** + * Get the current outfitting tab + * @return {string} the current outfitting tab + */ + getOutfittingTab() { + return this.outfittingTab; + } + /** * Retrieve the last router state from local storage * @return {Object} state State object containing state name and params From 73a75c69a3858bc88fc22dc7e2f6230d1e48efe6 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 20 Mar 2017 13:52:24 +0000 Subject: [PATCH 40/53] Make various components stateless --- .../anaconda-test-detailed-export-v4.json | 1 - package.json | 1 + src/app/components/Boost.jsx | 34 +-- src/app/components/Cargo.jsx | 53 +--- src/app/components/EngagementRange.jsx | 56 +--- src/app/components/Fuel.jsx | 50 +--- src/app/components/OutfittingSubpages.jsx | 7 +- src/app/components/Pips.jsx | 84 +----- src/app/components/ShipPicker.jsx | 52 ++-- src/app/components/ShipSummaryTable.jsx | 20 +- src/app/pages/OutfittingPage.jsx | 242 ++++++++++++++---- src/app/shipyard/Calculations.js | 12 +- 12 files changed, 286 insertions(+), 326 deletions(-) diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json index 93aa7a93..65893b57 100644 --- a/__tests__/fixtures/anaconda-test-detailed-export-v4.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -320,7 +320,6 @@ "shieldExplRes": 0.5, "shieldKinRes": 0.4, "shieldThermRes": -0.2, - "timeToDrain": 7.04, "crew": 3 } } diff --git a/package.json b/package.json index efc71869..c934693c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/components/Boost.jsx b/src/app/components/Boost.jsx index cc39d28e..10e60927 100644 --- a/src/app/components/Boost.jsx +++ b/src/app/components/Boost.jsx @@ -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 ( diff --git a/src/app/components/Cargo.jsx b/src/app/components/Cargo.jsx index bad1e2cf..96bbac83 100644 --- a/src/app/components/Cargo.jsx +++ b/src/app/components/Cargo.jsx @@ -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 ( -

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

    +

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

    ); + } else { + // We do not have a module, no value + results.push(); + } + } + } + + return ( +
    -

    {translate('engagement range')}: {formats.int(rangeLevel * maxRange)}{translate('m')}

    +

    {translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}

    @@ -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} diff --git a/src/app/components/Fuel.jsx b/src/app/components/Fuel.jsx index c3f9cc6b..8c8ea8d7 100644 --- a/src/app/components/Fuel.jsx +++ b/src/app/components/Fuel.jsx @@ -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 ( -

    {translate('fuel carried')}: {formats.f2(fuelLevel * fuelCapacity)}{units.T}

    +

    {translate('fuel carried')}: {formats.f1(fuel)}{units.T}

    ); - } - } - - return ( -
    - - + + ; } diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx index 7c9f86ec..29051318 100644 --- a/src/app/components/Pips.jsx +++ b/src/app/components/Pips.jsx @@ -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 ( @@ -349,15 +297,3 @@ export default class Pips extends TranslatedComponent { ); } } -// -// -// -// -// -// -// -// -// -// -// -// diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index cebfa173..3b048b86 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -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(
  • Stock
  • ); 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 (
    e.stopPropagation() }>
    diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 370cb03b..e4fb8edd 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -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
    {translate('capacity')} ({units.MJ}){formats.f1(sysCap)}{formats.f1(engCap)}{formats.f1(wepCap)}
    {translate('recharge')} ({units.MW}){formats.f1(sysRate * (sys / 4))}{formats.f1(engRate * (eng / 4))}{formats.f1(wepRate * (wep / 4))}
    - - + + @@ -72,8 +68,8 @@ export default class ShipSummaryTable extends TranslatedComponent { - - + + diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index b96c2ef3..583a5485 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -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 (
    @@ -386,10 +534,10 @@ export default class OutfittingPage extends Page { {/* Main tables */} - - - - + + + + {/* Control of ship and opponent */}
    @@ -397,28 +545,28 @@ export default class OutfittingPage extends Page {

    {translate('ship control')}

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

    {translate('opponent')}

    - +
    - +
    {/* Tabbed subpages */} diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 183ec30e..30432949 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -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; From 85f108556ada33fd71e76d6ae7112e9e9adfeb78 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Tue, 21 Mar 2017 00:41:33 +0000 Subject: [PATCH 41/53] Fix for ship summary --- src/app/components/Offence.jsx | 5 +- src/app/components/ShipSummaryTable.jsx | 74 +++++++++++++------------ src/app/i18n/en.js | 21 +++++++ src/app/pages/OutfittingPage.jsx | 4 +- 4 files changed, 66 insertions(+), 38 deletions(-) diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 1c823c3c..bb880d4d 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -140,6 +140,8 @@ export default class Offence extends TranslatedComponent { const opponentShields = Calc.shieldMetrics(opponent, 4); const opponentArmour = Calc.armourMetrics(opponent); + const timeToDrain = Calc.timeToDrainWep(ship, wep); + let absoluteShieldsSDps = 0; let explosiveShieldsSDps = 0; let kineticShieldsSDps = 0; @@ -243,7 +245,8 @@ export default class Offence extends TranslatedComponent {
    {translate('speed')}{translate('boost')}{translate('speed')}{translate('boost')} {translate('DPS')} {translate('EPS')} {translate('TTD')}
    { ship.canThrust() ? {int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']} : 0 }{ ship.canBoost() ? {int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']} : 0 }{ canThrust ? {int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']} : 0 }{ canBoost ? {int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']} : 0 } {f1(ship.totalDps)} {f1(ship.totalEps)} {timeToDrain === Infinity ? '∞' : time(timeToDrain)}
    -

    {translate('damage to opponent')}

    +

    {translate('offence metrics')}

    +

    {translate('PHRASE_TIME_TO_DRAIN_WEP')}
    {timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}

    {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}
    {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}

    {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}
    {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}

    diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index e4fb8edd..9212bb1b 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -11,11 +11,6 @@ export default class ShipSummaryTable extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, - 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, marker: React.PropTypes.string.isRequired, }; @@ -24,7 +19,7 @@ export default class ShipSummaryTable extends TranslatedComponent { * @return {React.Component} Summary table */ render() { - const { ship, fuel, eng, wep, cargo, boost } = this.props; + const { ship } = this.props; let { language, tooltip, termtip } = this.context; let translate = language.translate; let u = language.units; @@ -34,9 +29,12 @@ export default class ShipSummaryTable extends TranslatedComponent { const shieldGenerator = ship.findInternalByGroup('sg'); const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator }); - const timeToDrain = Calc.timeToDrainWep(ship, wep); + const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL'; + const timeToDrain = Calc.timeToDrainWep(ship, 4); const canThrust = ship.canThrust(); + const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; const canBoost = ship.canBoost(); + const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL'; return
    @@ -44,46 +42,52 @@ export default class ShipSummaryTable extends TranslatedComponent { - - - - - - - - + + + + + + + {/* */} - + + - + + + + + + - - - - + + - - - - - - - - - - - - + + + + + + + + + + + + + {/* */} - - + + + + diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index cf986c7b..6ed56ba4 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -57,6 +57,8 @@ export const terms = { TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons', PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in', TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons', + PHRASE_TIME_TO_DRAIN_WEP: 'Will drain WEP in', + TT_TIME_TO_DRAIN_WEP: 'Time to drain WEP capacitor with all weapons firing', TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons', TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons', TT_MODULE_ARMOUR: 'Armour protecting against module damage', @@ -68,6 +70,25 @@ export const terms = { TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty', TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m', + TT_SUMMARY_SPEED: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_SPEED_NONFUNCTIONAL: 'Thrusters powered off or over maximum mass', + TT_SUMMARY_BOOST: 'With full fuel tank and 4 pips to ENG', + TT_SUMMARY_BOOST_NONFUNCTIONAL: 'Power distributor not able to supply enough power to boost', + TT_SUMMARY_SHIELDS: 'Raw shield strength, including boosters', + TT_SUMMARY_SHIELDS_NONFUNCTIONAL: 'No shield generator or shield generator powered off', + TT_SUMMARY_INTEGRITY: 'Ship integrity, including bulkheads and hull reinforcement packages', + TT_SUMMARY_HULL_MASS: 'Mass of the hull prior to any modules being installed', + TT_SUMMARY_UNLADEN_MASS: 'Mass of the hull and modules prior to any fuel or cargo', + TT_SUMMARY_LADEN_MASS: 'Mass of the hull and modules with full fuel and cargo', + TT_SUMMARY_DPS: 'Damage per second with all weapons firing', + TT_SUMMARY_EPS: 'WEP capacitor consumed per second with all weapons firing', + TT_SUMMARY_TTD: 'Time to drain WEP capacitor with all weapons firing and 4 pips to WEP', + TT_SUMMARY_MAX_SINGLE_JUMP: 'Farthest possible jump range with no cargo and only enough fuel for the jump itself', + TT_SUMMARY_UNLADEN_SINGLE_JUMP: 'Farthest possible jump range with no cargo and a full fuel tank', + TT_SUMMARY_LADEN_SINGLE_JUMP: 'Farthest possible jump range with full cargo and a full fuel tank', + TT_SUMMARY_UNLADEN_TOTAL_JUMP: 'Farthest possible range with no cargo, a full fuel tank, and jumping as far as possible each time', + TT_SUMMARY_LADEN_TOTAL_JUMP: 'Farthest possible range with full cargo, a full fuel tank, and jumping as far as possible each time', + HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', // Other languages fallback to these values diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 583a5485..a4521982 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -497,7 +497,7 @@ export default class OutfittingPage extends Page { 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.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}`; return (
    @@ -533,7 +533,7 @@ export default class OutfittingPage extends Page {
    {/* Main tables */} - + From eb042b2778c42e955dd19bfcbac9f638ec41e153 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Tue, 21 Mar 2017 12:19:25 +0000 Subject: [PATCH 42/53] Tidy-ups for build changes --- src/app/components/OutfittingSubpages.jsx | 4 +-- src/app/components/ShipPicker.jsx | 3 +- src/app/pages/OutfittingPage.jsx | 35 ++++++++++++++++------- 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 665d2697..6a8bf2bd 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -126,7 +126,7 @@ export default class OutfittingSubpages extends TranslatedComponent { const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; Persist.setOutfittingTab('offence'); - const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; + const marker = `${ship.toString()}:${opponent.toString()}:${opponentBuild}:${engagementRange}`; return
    @@ -141,7 +141,7 @@ export default class OutfittingSubpages extends TranslatedComponent { const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; Persist.setOutfittingTab('defence'); - const marker = `${ship.toString()}:${opponent.name}:${opponentBuild}:${engagementRange}`; + const marker = `${ship.toString()}:${opponent.toString()}:${opponentBuild}:${engagementRange}`; return
    diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index 3b048b86..c2bcb9a4 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -57,7 +57,6 @@ export default class ShipPicker extends TranslatedComponent { _renderPickerMenu() { const { ship, build } = this.props; const _shipChange = this._shipChange; - const builds = Persist.getBuilds(); const buildList = []; for (let shipId of this.shipOrder) { @@ -68,7 +67,7 @@ export default class ShipPicker extends TranslatedComponent { if (builds[shipId]) { let buildNameOrder = Object.keys(builds[shipId]).sort(); for (let buildName of buildNameOrder) { - const buildSelected = ship.id == shipId && build == buildName; + const buildSelected = ship === shipId && build === buildName; shipBuilds.push(
  • {buildName}
  • ); } } diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index a4521982..d9452d74 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -279,15 +279,19 @@ export default class OutfittingPage extends Page { _saveBuild() { const { code, buildName, newBuildName, shipId } = this.state; - if (buildName === newBuildName) { - Persist.saveBuild(shipId, buildName, code); - this._updateRoute(shipId, buildName, code); - } else { - Persist.saveBuild(shipId, newBuildName, code); - this._updateRoute(shipId, newBuildName, code); - } + Persist.saveBuild(shipId, newBuildName, code); + this._updateRoute(shipId, newBuildName, code); - this.setState({ buildName: newBuildName, code, savedCode: code, title: this._getTitle(newBuildName) }); + let opponent, opponentBuild; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // This is a save of our current opponent build; update it + opponentBuild = newBuildName; + opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code); + } else { + opponentBuild = this.state.opponentBuild; + opponent = this.state.opponent; + } + this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, title: this._getTitle(newBuildName) }); } /** @@ -299,7 +303,7 @@ export default class OutfittingPage extends Page { Persist.deleteBuild(shipId, buildName); Persist.saveBuild(shipId, newBuildName, code); this._updateRoute(shipId, newBuildName, code); - this.setState({ buildName: newBuildName, code, savedCode: code }); + this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName }); } } @@ -338,8 +342,19 @@ export default class OutfittingPage extends Page { * Delete the build */ _deleteBuild() { - Persist.deleteBuild(this.state.shipId, this.state.buildName); + const { shipId, buildName } = this.state; + Persist.deleteBuild(shipId, buildName); + + let opponentBuild; + if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { + // Our current opponent has been deleted; revert to stock + opponentBuild = null; + } else { + opponentBuild = this.state.opponentBuild; + } Router.go(outfitURL(this.state.shipId)); + + this.setState({ opponentBuild }); } /** From 3f18987007b4d965378e4777fdec5c0aea8f57be Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Tue, 21 Mar 2017 14:46:35 +0000 Subject: [PATCH 43/53] Rewrite vertical bar chart to use recharts --- package.json | 5 +- src/app/Coriolis.jsx | 11 +- src/app/components/Defence.jsx | 2 +- src/app/components/VerticalBarChart.jsx | 150 ++++++++++-------------- 4 files changed, 71 insertions(+), 97 deletions(-) diff --git a/package.json b/package.json index c934693c..e018f952 100644 --- a/package.json +++ b/package.json @@ -90,16 +90,17 @@ }, "dependencies": { "babel-polyfill": "*", - "classnames": "^2.2.0", "browserify-zlib": "ipfs/browserify-zlib", + "classnames": "^2.2.0", "coriolis-data": "EDCD/coriolis-data", "d3": "4.6.0", "fbemitter": "^2.0.0", "lodash": "^4.15.0", "lz-string": "^1.4.4", - "react-number-editor": "Athanasius/react-number-editor.git#miggy", "react": "^15.0.1", "react-dom": "^15.0.1", + "react-number-editor": "Athanasius/react-number-editor.git#miggy", + "recharts": "^0.21.2", "superagent": "^1.4.0" } } diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index 5d676b5e..c0e3ea89 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -241,14 +241,19 @@ export default class Coriolis extends React.Component { /** * Show the term tip * @param {string} term Term or Phrase - * @param {Object} opts Options - dontCap, orientation (n,e,s,w) + * @param {Object} opts Options - dontCap, orientation (n,e,s,w) (can also be the event if no options supplied) * @param {SyntheticEvent} event Event + * @param {SyntheticEvent} e2 Alternative location for synthetic event from charts (where 'Event' is actually a chart index) */ - _termtip(term, opts, event) { - if (opts && opts.nativeEvent) { // Opts is a SyntheticEvent + _termtip(term, opts, event, e2) { + if (opts && opts.nativeEvent) { // Opts is the SyntheticEvent event = opts; opts = { cap: true }; } + if (e2 instanceof Object && e2.nativeEvent) { // E2 is the SyntheticEvent + event = e2; + } + this._tooltip(
    {this.state.language.translate(term)}
    , event.currentTarget.getBoundingClientRect(), diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index 56a1bafc..d365206c 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -165,7 +165,7 @@ export default class Defence extends TranslatedComponent {

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

    - +

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

    diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index 617b1b4b..b8a709cf 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -1,12 +1,11 @@ -import React, { Component } from 'react'; -import Measure from 'react-measure'; -import * as d3 from 'd3'; import TranslatedComponent from './TranslatedComponent'; +import React, { PropTypes } from 'react'; +import Measure from 'react-measure'; +import { BarChart, Bar, XAxis, YAxis } from 'recharts'; const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; const LABEL_COLOUR = '#000000'; - -const margin = { top: 10, right: 0, bottom: 0, left: 55 }; +const AXIS_COLOUR = '#C06400'; const ASPECT = 1; @@ -20,8 +19,8 @@ const merge = function(one, two) { export default class VerticalBarChart extends TranslatedComponent { static propTypes = { - data : React.PropTypes.array.isRequired, - yMax : React.PropTypes.number + data : PropTypes.array.isRequired, + yMax : PropTypes.number }; /** @@ -32,6 +31,8 @@ export default class VerticalBarChart extends TranslatedComponent { constructor(props, context) { super(props); + this._termtip = this._termtip.bind(this); + this.state = { dimensions: { width: 300, @@ -41,97 +42,64 @@ export default class VerticalBarChart extends TranslatedComponent { } /** - * Render the graph - * @param {Object} props React Component properties - */ - _renderGraph(props) { - let { width, height } = this.state.dimensions; - const { tooltip, termtip } = this.context; - - width = width - margin.left - margin.right, - height = width * ASPECT - margin.top - margin.bottom; - - // X axis is a band scale with values being 'label' - this.x = d3.scaleBand(); - this.x.domain(this.props.data.map(d => d.label)).padding(0.2); - this.xAxis = d3.axisBottom(this.x).tickValues(this.props.data.map(d => d.label)); - this.x.range([0, width]); - - // Y axis is a numeric scale with values being 'value' - this.y = d3.scaleLinear(); - if (props.yMax) { - // Fixed maximum value (unless we go off the scale) - const localMax = d3.max(this.props.data, d => d.value); - this.y.domain([0, localMax > props.yMax ? localMax : props.yMax]); - } else { - this.y.domain([0, d3.max(this.props.data, d => d.value)]); - } - this.yAxis = d3.axisLeft(this.y); - this.y.range([height, 0]); - - let svg = d3.select(this.svg).select('g'); - - svg.selectAll('rect').remove(); - svg.selectAll('text').remove(); - - svg.select('.x.axis').remove(); - svg.select('.y.axis').remove(); - - svg.append('g') - .attr('class', 'x axis') - .attr('transform', `translate(0, ${height})`) - .call(this.xAxis); - - svg.append('g') - .attr('class', 'y axis') - .call(this.yAxis) - .attr('fill', CORIOLIS_COLOURS[0]); - - svg.selectAll('rect.bar') - .data(props.data) - .enter().append('rect') - .attr('class', 'bar') - .attr('x', d => this.x(d.label)) - .attr('width', this.x.bandwidth()) - .attr('y', d => this.y(d.value)) - .attr('height', d => height - this.y(d.value)) - .attr('fill', CORIOLIS_COLOURS[0]); - - svg.selectAll('text.bar') - .data(props.data) - .enter().append('text') - .attr('class', 'bar') - .attr('text-anchor', 'middle') - .attr('x', 100) - .attr('y', 100) - .attr('stroke-width', '0px') - .attr('fill', LABEL_COLOUR) - .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2) - .attr('y', d => this.y(d.value) + 15) - .text(d => d.value); - } - - /** - * Render the component - * @returns {object} Markup + * Render the bar chart + * @returns {Object} the markup */ render() { - const { width } = this.state.dimensions; - const translate = `translate(${margin.left}, ${margin.top})`; + const { width, height } = this.state.dimensions; + const { tooltip, termtip } = this.context; - const height = width * ASPECT; - - this._renderGraph(this.props); + // Calculate maximum for Y + let dataMax = Math.max(...this.props.data.map(d => d.value)); + if (dataMax == -Infinity) dataMax = 0; + let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; + const localMax = Math.max(dataMax, yMax); return ( - { this.setState({ dimensions }); }}> -
    - { this.x ? - this.svg = ref} width={width} height={height}> - - : null } + this.setState({ dimensions }) }> +
    + + + + } fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/> +
    ); } + + /** + * Generate a term tip + * @param {Object} d the data + * @param {number} i the index + * @param {Object} e the event + * @returns {Object} termtip markup + */ + _termtip(d, i, e) { + if (this.props.data[i].tooltip) { + return this.context.termtip(this.props.data[i].tooltip, e); + } else { + return null; + } + } } + +/** + * A label that displays the value within the bar of the chart + */ +const ValueLabel = React.createClass({ + propTypes: { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + value: PropTypes.number + }, + + render() { + const { x, y, payload, value } = this.props; + + return ( + {value} + ); + } +}); From 32fb66139a42e314e98bf795aedb32617a98c656 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 09:20:51 +0000 Subject: [PATCH 44/53] Take base hull reinforcement resistances in to account when calculating modifiers --- ChangeLog.md | 1 + src/app/utils/CompanionApiUtils.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index eff8db7f..c03a9d5b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -8,6 +8,7 @@ * Show integrity value for relevant modules * Reset old modification values when a new roll is applied * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost + * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js index 461886cb..6c6a2272 100644 --- a/src/app/utils/CompanionApiUtils.js +++ b/src/app/utils/CompanionApiUtils.js @@ -384,15 +384,16 @@ function _addModifications(module, modifiers, blueprint, grade) { } // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted. + // In addition, the modification is based off the inherent resistance of the module if (module.grp === 'hr') { if (module.getModValue('explres')) { - module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); + module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); } if (module.getModValue('kinres')) { - module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); + module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000); } if (module.getModValue('thermres')) { - module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); + module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000); } } From 75a4e54453010e16293c15884d7d087b75ec66e3 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 10:51:23 +0000 Subject: [PATCH 45/53] Add tooltips for blueprints --- ChangeLog.md | 1 + src/app/components/ModificationsMenu.jsx | 74 ++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index c03a9d5b..08bceac1 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ * Reset old modification values when a new roll is applied * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages + * Add tooltip for blueprints providing details of the features they alter #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index ee961d29..52840fdc 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -43,7 +43,7 @@ export default class ModificationsMenu extends TranslatedComponent { */ _initState(props, context) { let { m } = props; - const { language } = context; + const { language, tooltip, termtip } = context; const translate = language.translate; // Set up the blueprints @@ -52,7 +52,8 @@ export default class ModificationsMenu extends TranslatedComponent { for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); const key = blueprintName + ':' + grade; - blueprints.push(
    {translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
    ); + const tooltipContent = this._blueprintTooltip(translate, Modifications.blueprints[blueprintName].grades[grade].features); + blueprints.push(
    {translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
    ); } } @@ -76,6 +77,51 @@ export default class ModificationsMenu extends TranslatedComponent { return { blueprintMenuOpened, blueprints, modifications, specialMenuOpened, specials }; } + /** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} features The features of the blueprint + * @returns {Object} The react components + */ + _blueprintTooltip(translate, features) + { + const results = []; + for (const feature in features) { + const featureIsBeneficial = this._isBeneficial(feature, features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = features[feature][0]; + let upperBound = features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const range = `${lowerBound}${symbol} - ${upperBound}${symbol}`; + results.push(
    ); + } + } + + return ( +
    {translate('speed')} {translate('boost')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('hrd')}{translate('arm')}{translate('shld')}{translate('mass')}{translate('jump range')}{translate('shield')}{translate('integrity')}{translate('DPS')}{translate('EPS')}{translate('TTD')}{translate('HPS')}{translate('cargo')} {translate('fuel')}{translate('jump range')}{translate('mass')}{translate('hrd')} {translate('crew')}{translate('MLF')}{translate('MLF')}
    {translate('max')}{translate('unladen')}{translate('laden')}{translate('total unladen')}{translate('total laden')} {translate('hull')}{translate('unladen')}{translate('laden')}{translate('single')}{translate('total')}{translate('unladen')}{translate('laden')}
    { canThrust ? {int(ship.calcSpeed(eng, fuel, cargo, false))}{u['m/s']} : 0 }{ canBoost ? {int(ship.calcSpeed(eng, fuel, cargo, true))}{u['m/s']} : 0 }{f1(ship.totalDps)}{f1(ship.totalEps)}{timeToDrain === Infinity ? '∞' : time(timeToDrain)}{f1(ship.totalHps)}{int(ship.hardness)}{int(ship.armour)}{int(ship.shield)}{u.MJ}{ship.hullMass}{u.T}{int(ship.unladenMass)}{u.T}{int(ship.ladenMass)}{u.T}{ canThrust ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']} : 0 }{ canBoost ? {int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']} : 0 }{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}{int(ship.shield)}{u.MJ}{int(ship.armour)}{f1(ship.totalDps)}{f1(ship.totalEps)}{timeToDrain === Infinity ? '∞' : time(timeToDrain)}{f1(ship.totalHps)}{round(ship.cargoCapacity)}{u.T} {round(ship.fuelCapacity)}{u.T}{f2(Calc.jumpRange(ship.unladenMass + fuel + cargo, ship.standard[2].m, fuel))}{u.LY}{f2(Calc.totalJumpRange(ship.unladenMass + fuel + cargo, ship.standard[2].m, fuel))}{u.LY}{ship.hullMass}{u.T}{int(ship.unladenMass)}{u.T}{int(ship.ladenMass)}{u.T}{int(ship.hardness)} {ship.crew} {ship.masslock}
    {translate(feature)}{lowerBound}{symbol}{upperBound}{symbol}
    + + + + + + + + + {results} + +
    {translate('effect')}{translate('worst')}{translate('best')}
    + ); + } + /** * Initialise the modifications * @param {Object} props React Component properties @@ -244,6 +290,19 @@ export default class ModificationsMenu extends TranslatedComponent { this.props.onChange(); } + /** + * Is this feature beneficial? + * + */ + _isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } + } + /** * Reset modification information */ @@ -276,11 +335,11 @@ export default class ModificationsMenu extends TranslatedComponent { let blueprintLabel; let haveBlueprint = false; + let blueprintTooltip; if (m.blueprint && !isEmpty(m.blueprint)) { blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; haveBlueprint = true; - } else { - blueprintLabel = translate('PHRASE_SELECT_BLUEPRINT'); + blueprintTooltip = this._blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features); } let specialLabel; @@ -291,7 +350,7 @@ export default class ModificationsMenu extends TranslatedComponent { } const showBlueprintsMenu = blueprintMenuOpened; - const showSpecial = haveBlueprint && this.state.specials.length > 0; + const showSpecial = haveBlueprint && this.state.specials.length > 0 && !blueprintMenuOpened; const showSpecialsMenu = specialMenuOpened; const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened; const showReset = !blueprintMenuOpened && !specialMenuOpened; @@ -303,7 +362,10 @@ export default class ModificationsMenu extends TranslatedComponent { onClick={(e) => e.stopPropagation() } onContextMenu={stopCtxPropagation} > -
    {blueprintLabel}
    + { haveBlueprint ? +
    {blueprintLabel}
    + : +
    {translate('PHRASE_SELECT_BLUEPRINT')}
    } { showBlueprintsMenu ? this.state.blueprints : null } { showSpecial ?
    {specialLabel}
    : null } { showSpecialsMenu ? this.state.specials : null } From a1a17bc8369bae42618f7904ecad10f4a66c81c5 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 11:44:08 +0000 Subject: [PATCH 46/53] Add blueprint info to modification tooltip --- src/app/components/HardpointSlot.jsx | 7 +++ src/app/components/InternalSlot.jsx | 7 +++ src/app/components/ModificationsMenu.jsx | 67 ++-------------------- src/app/components/StandardSlot.jsx | 7 +++ src/app/utils/BlueprintFunctions.js | 72 ++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 62 deletions(-) create mode 100644 src/app/utils/BlueprintFunctions.js diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 4726f526..1a48a3a5 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** @@ -51,6 +52,12 @@ export default class HardpointSlot extends Slot { if (m.blueprint.special && m.blueprint.special.id >= 0) { modTT += ', ' + translate(m.blueprint.special.name); } + modTT = ( +
    +
    {modTT}
    + {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
    + ); } return
    diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index ec580311..562f7555 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -4,6 +4,7 @@ import Persist from '../stores/Persist'; import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Internal Slot @@ -30,6 +31,12 @@ export default class InternalSlot extends Slot { let modTT = translate('modified'); if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + modTT = ( +
    +
    {modTT}
    + {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
    + ); } let mass = m.getMass() || m.cargo || m.fuel || 0; diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index 52840fdc..8faea959 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -5,6 +5,7 @@ import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions'; import cn from 'classnames'; import { Modifications } from 'coriolis-data/dist'; import Modification from './Modification'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Modifications menu @@ -52,7 +53,7 @@ export default class ModificationsMenu extends TranslatedComponent { for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); const key = blueprintName + ':' + grade; - const tooltipContent = this._blueprintTooltip(translate, Modifications.blueprints[blueprintName].grades[grade].features); + const tooltipContent = blueprintTooltip(translate, Modifications.blueprints[blueprintName].grades[grade].features); blueprints.push(
    {translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
    ); } } @@ -77,51 +78,6 @@ export default class ModificationsMenu extends TranslatedComponent { return { blueprintMenuOpened, blueprints, modifications, specialMenuOpened, specials }; } - /** - * Generate a tooltip with details of a blueprint's effects - * @param {Object} features The features of the blueprint - * @returns {Object} The react components - */ - _blueprintTooltip(translate, features) - { - const results = []; - for (const feature in features) { - const featureIsBeneficial = this._isBeneficial(feature, features[feature]); - const featureDef = Modifications.modifications[feature]; - if (!featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let lowerBound = features[feature][0]; - let upperBound = features[feature][1]; - if (featureDef.type === 'percentage') { - lowerBound = Math.round(lowerBound * 1000) / 10; - upperBound = Math.round(upperBound * 1000) / 10; - } - const range = `${lowerBound}${symbol} - ${upperBound}${symbol}`; - results.push(
    {translate(feature)}{lowerBound}{symbol}{upperBound}{symbol}
    - - - - - - - - - {results} - -
    {translate('effect')}{translate('worst')}{translate('best')}
    - ); - } - /** * Initialise the modifications * @param {Object} props React Component properties @@ -290,19 +246,6 @@ export default class ModificationsMenu extends TranslatedComponent { this.props.onChange(); } - /** - * Is this feature beneficial? - * - */ - _isBeneficial(feature, values) { - const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); - if (Modifications.modifications[feature].higherbetter) { - return !fact; - } else { - return fact; - } - } - /** * Reset modification information */ @@ -335,11 +278,11 @@ export default class ModificationsMenu extends TranslatedComponent { let blueprintLabel; let haveBlueprint = false; - let blueprintTooltip; + let blueprintTt; if (m.blueprint && !isEmpty(m.blueprint)) { blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; haveBlueprint = true; - blueprintTooltip = this._blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features); + blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features); } let specialLabel; @@ -363,7 +306,7 @@ export default class ModificationsMenu extends TranslatedComponent { onContextMenu={stopCtxPropagation} > { haveBlueprint ? -
    {blueprintLabel}
    +
    {blueprintLabel}
    :
    {translate('PHRASE_SELECT_BLUEPRINT')}
    } { showBlueprintsMenu ? this.state.blueprints : null } diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 2dade7e1..8ec68235 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -8,6 +8,7 @@ import ModificationsMenu from './ModificationsMenu'; import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import { blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Standard Slot @@ -53,6 +54,12 @@ export default class StandardSlot extends TranslatedComponent { let modTT = translate('modified'); if (m && m.blueprint && m.blueprint.name) { modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; + modTT = ( +
    +
    {modTT}
    + {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)} +
    + ); } if (!selected) { diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js new file mode 100644 index 00000000..ece22eed --- /dev/null +++ b/src/app/utils/BlueprintFunctions.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} features The features of the blueprint + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, features, m) +{ + const results = []; + for (const feature in features) { + const featureIsBeneficial = isBeneficial(feature, features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = features[feature][0]; + let upperBound = features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const range = `${lowerBound}${symbol} - ${upperBound}${symbol}`; + if (m) { + // We have a module - add in the current value + let current = m.getModValue(feature); + if (featureDef.type === 'percentage') { + current = Math.round(current / 10) / 10; + } + results.push(
    {translate(feature)}{lowerBound}{symbol}{current}{symbol}{upperBound}{symbol}
    {translate(feature)}{lowerBound}{symbol}{upperBound}{symbol}
    + + + + + {m ? : null } + + + + + {results} + +
    {translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
    + ); +} + +/** + * Is this blueprint feature beneficial? + * + */ +export function isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } +} From 82ce86a37419811e4daf8f5a74c88a14ca84726d Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 13:55:45 +0000 Subject: [PATCH 47/53] Updates --- ChangeLog.md | 1 + src/app/components/HardpointSlot.jsx | 2 +- src/app/components/StandardSlot.jsx | 2 +- src/app/i18n/en.js | 5 +++-- src/app/pages/OutfittingPage.jsx | 25 +++++++++++++++++++++---- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 08bceac1..ec2992c6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ * Ensure that boost value is set correctly when modifications to power distributor enable/disable boost * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages * Add tooltip for blueprints providing details of the features they alter + * Use opponent's saved pips if available #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 1a48a3a5..bca5ecbc 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -81,7 +81,7 @@ export default class HardpointSlot extends Slot { { m.getHps() ?
    {translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
    : null } { m.getDps() && m.getEps() ?
    {translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
    : null } { m.getRoF() ?
    {translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
    : null } - { m.getRange() ?
    {translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}
    : null } + { m.getRange() ?
    {translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}
    : null } { m.getScanTime() ?
    {translate('scantime')} {formats.f1(m.getScanTime())}{u.s}
    : null } { m.getFalloff() ?
    {translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}
    : null } { m.getShieldBoost() ?
    +{formats.pct1(m.getShieldBoost())}
    : null } diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 8ec68235..30d5502f 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -100,7 +100,7 @@ export default class StandardSlot extends TranslatedComponent { { m.getMinMass() ?
    {translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}
    : null } { m.getOptMass() ?
    {translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}
    : null } { m.getMaxMass() ?
    {translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
    : null } - { m.getRange() ?
    {translate('range')}: {formats.f2(m.getRange())}{units.km}
    : null } + { m.getRange() ?
    {translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}
    : null } { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } { m.getThermalEfficiency() ?
    {translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}
    : null } { m.getPowerGeneration() > 0 ?
    {translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}
    : null } diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 6ed56ba4..36fd123e 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -230,8 +230,7 @@ export const terms = { regen: 'Regeneration rate', reload: 'Reload', rof: 'Rate of fire', - scanangle: 'Scan angle', - scanrange: 'Scan range', + angle: 'Scan angle', scantime: 'Scan time', shield: 'Shield', shieldboost: 'Shield boost', @@ -253,6 +252,8 @@ export const terms = { optmul_sg: 'Optimal strength', maxmul_sg: 'Minimum strength', + range_s: 'Typical emission range', + // Damage types absolute: 'Absolute', explosive: 'Explosive', diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index d9452d74..11b4b71a 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -90,7 +90,7 @@ 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); + const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code); return { error: null, title: this._getTitle(buildName), @@ -109,6 +109,9 @@ export default class OutfittingPage extends Page { cargo, opponent, opponentBuild, + opponentSys, + opponentEng, + opponentWep, engagementRange }; } @@ -167,7 +170,10 @@ export default class OutfittingPage extends Page { 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 opponentSys = 2; + let opponentEng = 2; + let opponentWep = 2; + let opponentBuild; let engagementRange = 1000; // Obtain updates from code, if available @@ -187,8 +193,19 @@ export default class OutfittingPage extends Page { 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])); + const opponentCode = Persist.getBuild(shipId, control[7]); + opponent.buildFrom(opponentCode); opponentBuild = control[7]; + if (opponentBuild) { + // Obtain opponent's sys/eng/wep pips from their code + const opponentParts = opponentCode.split('.'); + if (opponentParts.length >= 5) { + const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/'); + opponentSys = parseFloat(opponentControl[0]); + opponentEng = parseFloat(opponentControl[1]); + opponentWep = parseFloat(opponentControl[2]); + } + } } else { // Ship is a stock build opponent.buildWith(Ships[shipId].defaults); @@ -198,7 +215,7 @@ export default class OutfittingPage extends Page { } } - return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange }; + return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange }; } /** From c7ea1eb95aa7015fd180f55e914b4212633aa30b Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 14:19:21 +0000 Subject: [PATCH 48/53] Use opponent's pip info --- src/app/components/Defence.jsx | 13 +++++++------ src/app/components/Offence.jsx | 7 ++++--- src/app/components/OutfittingSubpages.jsx | 21 ++++++++++++--------- src/app/components/ShipPicker.jsx | 2 +- src/app/pages/OutfittingPage.jsx | 15 ++++++++++++--- src/app/shipyard/Calculations.js | 22 ++++++---------------- 6 files changed, 42 insertions(+), 38 deletions(-) diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx index d365206c..7d6f5e78 100644 --- a/src/app/components/Defence.jsx +++ b/src/app/components/Defence.jsx @@ -18,7 +18,8 @@ export default class Defence extends TranslatedComponent { ship: React.PropTypes.object.isRequired, opponent: React.PropTypes.object.isRequired, engagementrange: React.PropTypes.number.isRequired, - sys: React.PropTypes.number.isRequired + sys: React.PropTypes.number.isRequired, + opponentWep: React.PropTypes.number.isRequired }; /** @@ -28,7 +29,7 @@ export default class Defence extends TranslatedComponent { constructor(props) { super(props); - const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.engagementrange); + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.opponentWep, props.engagementrange); this.state = { shield, armour, shielddamage, armourdamage }; } @@ -39,7 +40,7 @@ export default class Defence extends TranslatedComponent { */ componentWillReceiveProps(nextProps) { if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) { - const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.engagementrange); + const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.opponentWep, nextProps.engagementrange); this.setState({ shield, armour, shielddamage, armourdamage }); } return true; @@ -50,7 +51,7 @@ export default class Defence extends TranslatedComponent { * @return {React.Component} contents */ render() { - const { ship, sys } = this.props; + const { ship, sys, opponentWep } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; const { shield, armour, shielddamage, armourdamage } = this.state; @@ -155,7 +156,7 @@ export default class Defence extends TranslatedComponent {

    {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(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate()))}

    +

    {translate('PHRASE_TIME_TO_LOSE_SHIELDS')}
    {shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

    {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)}

    @@ -176,7 +177,7 @@ export default class Defence extends TranslatedComponent {

    {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('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate()))}

    +

    {translate('PHRASE_TIME_TO_LOSE_ARMOUR')}
    {armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}

    {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)}

    diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index bb880d4d..0faa5c82 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -55,7 +55,8 @@ export default class Offence extends TranslatedComponent { ship: React.PropTypes.object.isRequired, opponent: React.PropTypes.object.isRequired, engagementrange: React.PropTypes.number.isRequired, - wep: React.PropTypes.number.isRequired + wep: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired }; /** @@ -67,7 +68,7 @@ export default class Offence extends TranslatedComponent { this._sort = this._sort.bind(this); - const damage = Calc.offenceMetrics(props.ship, props.opponent, props.eng, props.engagementrange); + const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange); this.state = { predicate: 'n', desc: true, @@ -82,7 +83,7 @@ export default class Offence extends TranslatedComponent { */ componentWillReceiveProps(nextProps) { if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) { - const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.engagementrange); + const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange); this.setState({ damage }); } return true; diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 6a8bf2bd..5eb2abb1 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -31,7 +31,10 @@ export default class OutfittingSubpages extends TranslatedComponent { boost: React.PropTypes.bool.isRequired, engagementRange: React.PropTypes.number.isRequired, opponent: React.PropTypes.object.isRequired, - opponentBuild: React.PropTypes.string + opponentBuild: React.PropTypes.string, + opponentSys: React.PropTypes.number.isRequired, + opponentEng: React.PropTypes.number.isRequired, + opponentWep: React.PropTypes.number.isRequired, }; /** @@ -80,7 +83,7 @@ export default class OutfittingSubpages extends TranslatedComponent { * @return {React.Component} Tab contents */ _profilesTab() { - const { ship, opponent, cargo, fuel, eng, boost, engagementRange } = this.props; + const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props; const { translate } = this.context.language; let realBoost = boost && ship.canBoost(); Persist.setOutfittingTab('profiles'); @@ -88,7 +91,7 @@ export default class OutfittingSubpages extends TranslatedComponent { 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()}`; - const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}`; + const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`; return
    @@ -123,13 +126,13 @@ export default class OutfittingSubpages extends TranslatedComponent { * @return {React.Component} Tab contents */ _offenceTab() { - const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props; Persist.setOutfittingTab('offence'); - const marker = `${ship.toString()}:${opponent.toString()}:${opponentBuild}:${engagementRange}`; + const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`; return
    - +
    ; } @@ -138,13 +141,13 @@ export default class OutfittingSubpages extends TranslatedComponent { * @return {React.Component} Tab contents */ _defenceTab() { - const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild } = this.props; + const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props; Persist.setOutfittingTab('defence'); - const marker = `${ship.toString()}:${opponent.toString()}:${opponentBuild}:${engagementRange}`; + const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`; return
    - +
    ; } diff --git a/src/app/components/ShipPicker.jsx b/src/app/components/ShipPicker.jsx index c2bcb9a4..93505b8d 100644 --- a/src/app/components/ShipPicker.jsx +++ b/src/app/components/ShipPicker.jsx @@ -45,7 +45,7 @@ export default class ShipPicker extends TranslatedComponent { this._closeMenu(); // Ensure that the ship has changed - if (ship !== this.props.ship || this.build !== this.props.build) { + if (ship !== this.props.ship || build !== this.props.build) { this.props.onChange(ship, build); } } diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 11b4b71a..81372ec8 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -299,16 +299,22 @@ export default class OutfittingPage extends Page { Persist.saveBuild(shipId, newBuildName, code); this._updateRoute(shipId, newBuildName, code); - let opponent, opponentBuild; + let opponent, opponentBuild, opponentSys, opponentEng, opponentWep; if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) { // This is a save of our current opponent build; update it opponentBuild = newBuildName; opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code); + opponentSys = this.state.sys; + opponentEng = this.state.eng; + opponentWep = this.state.wep; } else { opponentBuild = this.state.opponentBuild; opponent = this.state.opponent; + opponentSys = this.state.opponentSys; + opponentEng = this.state.opponentEng; + opponentWep = this.state.opponentWep; } - this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, title: this._getTitle(newBuildName) }); + this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) }); } /** @@ -507,7 +513,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, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = state, + { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state, hide = tooltip.bind(null, null), menu = this.props.currentMenu, shipUpdated = this._shipUpdated, @@ -616,6 +622,9 @@ export default class OutfittingPage extends Page { engagementRange={engagementRange} opponent={opponent} opponentBuild={opponentBuild} + opponentSys={opponentSys} + opponentEng={opponentEng} + opponentWep={opponentWep} />
    ); diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 30432949..3b806710 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -533,10 +533,11 @@ export function armourMetrics(ship) { * @param {Object} ship The ship * @param {Object} opponent The opponent ship * @param {int} sys The pips to SYS + * @param {int} opponentWep The pips to pponent's WEP * @param {int} engagementrange The range between the ship and opponent * @returns {Object} Defence metrics */ -export function defenceMetrics(ship, opponent, sys, engagementrange) { +export function defenceMetrics(ship, opponent, sys, opponentWep, engagementrange) { // Obtain the shield metrics const shield = this.shieldMetrics(ship, sys); @@ -572,15 +573,16 @@ export function defenceMetrics(ship, opponent, sys, engagementrange) { * @param {Object} ship The ship * @param {Object} opponent The opponent ship * @param {int} wep The pips to WEP + * @param {int} opponentSys The pips to opponent's SYS * @param {int} engagementrange The range between the ship and opponent * @returns {array} Offence metrics */ -export function offenceMetrics(ship, opponent, wep, engagementrange) { +export function offenceMetrics(ship, opponent, wep, opponentSys, engagementrange) { // Per-weapon and total damage const damage = []; // Obtain the opponent's shield and armour metrics - const opponentShields = this.shieldMetrics(opponent, 4); + const opponentShields = this.shieldMetrics(opponent, opponentSys); const opponentArmour = this.armourMetrics(opponent); // Per-weapon and total damage to shields @@ -612,18 +614,6 @@ export function offenceMetrics(ship, opponent, wep, engagementrange) { } return damage; - - const shielddamage = opponentShields.generator ? { - absolute: { - weapon1: 10, - weapon2: 10, - weapon3: 10, - weapon4: 10, - total: 40 - } - } : {}; - - return damage; } /** @@ -632,7 +622,7 @@ export function offenceMetrics(ship, opponent, wep, engagementrange) { * @returns {integer} the resistance for the given pips */ export function sysResistance(sys) { - return Math.pow(sys,0.85) * 0.6 / Math.pow(4,0.85); + return Math.pow(sys, 0.85) * 0.6 / Math.pow(4, 0.85); } /** From d15e49f315827623d227b8d7148d95a74839c7bb Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 17:53:26 +0000 Subject: [PATCH 49/53] Ignore rpshot for eps and hps --- ChangeLog.md | 1 + src/app/components/ModificationsMenu.jsx | 24 +++++-------- src/app/shipyard/Module.js | 8 ++--- src/app/shipyard/Ship.js | 33 ++++++++++++++--- src/app/utils/BlueprintFunctions.js | 46 ++++++++++++++++++++++++ src/app/utils/CompanionApiUtils.js | 5 +-- 6 files changed, 92 insertions(+), 25 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index ec2992c6..0158383b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -11,6 +11,7 @@ * Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages * Add tooltip for blueprints providing details of the features they alter * Use opponent's saved pips if available + * Ignore rounds per shot for EPS and HPS calculations; it's already factored in to the numbers #2.2.19 * Power management panel now displays modules in descending order of power usage by default diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index 8faea959..ee950579 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -5,7 +5,7 @@ import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions'; import cn from 'classnames'; import { Modifications } from 'coriolis-data/dist'; import Modification from './Modification'; -import { blueprintTooltip } from '../utils/BlueprintFunctions'; +import { getBlueprint, blueprintTooltip } from '../utils/BlueprintFunctions'; /** * Modifications menu @@ -51,10 +51,11 @@ export default class ModificationsMenu extends TranslatedComponent { let blueprints = []; for (const blueprintName in Modifications.modules[m.grp].blueprints) { for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { - const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); + const close = this._blueprintSelected.bind(this, blueprintName, grade); const key = blueprintName + ':' + grade; - const tooltipContent = blueprintTooltip(translate, Modifications.blueprints[blueprintName].grades[grade].features); - blueprints.push(
    {translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}
    ); + const blueprint = getBlueprint(blueprintName, m); + const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade].features); + blueprints.push(
    {translate(blueprint.name + ' grade ' + grade)}
    ); } } @@ -105,12 +106,12 @@ export default class ModificationsMenu extends TranslatedComponent { /** * Activated when a blueprint is selected - * @param {int} blueprintId The ID of the selected blueprint - * @param {int} grade The grade of the selected blueprint + * @param {int} fdname The Frontier name of the blueprint + * @param {int} grade The grade of the selected blueprint */ - _blueprintSelected(blueprintId, grade) { + _blueprintSelected(fdname, grade) { const { m } = this.props; - const blueprint = Object.assign({}, _.find(Modifications.blueprints, function(o) { return o.id === blueprintId; })); + const blueprint = getBlueprint(fdname, m); blueprint.grade = grade; m.blueprint = blueprint; @@ -155,13 +156,6 @@ export default class ModificationsMenu extends TranslatedComponent { * @param {number} value The value of the roll */ _setRollResult(ship, m, featureName, value) { - if (Modifications.modifications[featureName].method !== 'overwrite') { - if (m.grp == 'sb' && featureName == 'shieldboost') { - // Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here - value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1; - } - } - if (Modifications.modifications[featureName].type == 'percentage') { ship.setModification(m, featureName, value * 10000); } else if (Modifications.modifications[featureName].type == 'numeric') { diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index e013609b..2ea29321 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -554,10 +554,10 @@ export default class Module { getEps() { // EPS is a synthetic value let distdraw = this.getDistDraw(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return distdraw * rpshot * rof; + return distdraw * rof; } /** @@ -567,10 +567,10 @@ export default class Module { getHps() { // HPS is a synthetic value let heat = this.getThermalLoad(); - let rpshot = this.roundspershot || 1; + // We don't use rpshot here as dist draw is per combined shot let rof = this.getRoF() || 1; - return heat * rpshot * rof; + return heat * rof; } /** diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index ff564bbb..d207279e 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1,6 +1,7 @@ import * as Calc from './Calculations'; import * as ModuleUtils from './ModuleUtils'; import * as Utils from '../utils/UtilityFunctions'; +import { getBlueprint } from '../utils/BlueprintFunctions'; import Module from './Module'; import LZString from 'lz-string'; import * as _ from 'lodash'; @@ -588,7 +589,13 @@ export default class Ship { this.bulkheads.m = null; this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; - this.bulkheads.m.blueprint = blueprints && blueprints[0] ? blueprints[0] : {}; + if (blueprints && blueprints[0]) { + this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m); + this.bulkheads.m.blueprint.grade = blueprints[0].grade; + this.bulkheads.m.blueprint.special = blueprints[0].special; + } else { + this.bulkheads.m.blueprint = {}; + } this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; @@ -602,7 +609,13 @@ export default class Ship { let module = ModuleUtils.standard(i, comps.standard[i]); if (module != null) { module.mods = mods && mods[i + 1] ? mods[i + 1] : {}; - module.blueprint = blueprints && blueprints[i + 1] ? blueprints[i + 1] : {}; + if (blueprints && blueprints[i + 1]) { + module.blueprint = getBlueprint(blueprints[i + 1].fdname, module); + module.blueprint.grade = blueprints[i + 1].grade; + module.blueprint.special = blueprints[i + 1].special; + } else { + module.blueprint = {}; + } } this.use(standard[i], module, true); } @@ -624,7 +637,13 @@ export default class Ship { let module = ModuleUtils.hardpoints(comps.hardpoints[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(hps[i], module, true); } @@ -644,7 +663,13 @@ export default class Ship { let module = ModuleUtils.internal(comps.internal[i]); if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; - module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; + if (blueprints && blueprints[cl + i]) { + module.blueprint = getBlueprint(blueprints[cl + i].fdname, module); + module.blueprint.grade = blueprints[cl + i].grade; + module.blueprint.special = blueprints[cl + i].special; + } else { + module.blueprint = {}; + } } this.use(internal[i], module, true); } diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index ece22eed..0389f2cf 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -70,3 +70,49 @@ export function isBeneficial(feature, values) { return fact; } } + +/** + * Get a blueprint with a given name and an optional module + * @param {string} name The name of the blueprint + * @param {Object} module The module for which to obtain this blueprint + * @returns {Object} The matching blueprint + */ +export function getBlueprint(name, module) { + // Start with a copy of the blueprint + const blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name])); + if (module) { + if (module.grp === 'bh') { + // Bulkheads need to have their resistances altered + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'explres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.explres); + blueprint.grades[grade].features[feature][1] *= (1 - module.explres); + } + if (feature === 'kinres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.kinres); + blueprint.grades[grade].features[feature][1] *= (1 - module.kinres); + } + if (feature === 'thermres') { + blueprint.grades[grade].features[feature][0] *= (1 - module.thermres); + blueprint.grades[grade].features[feature][1] *= (1 - module.thermres); + } + } + } + } + if (module.grp === 'sb') { + // Shield boosters are treated internally as straight modifiers, so rather than (for example) + // being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look + // accurate as per the information in Elite + for (const grade in blueprint.grades) { + for (const feature in blueprint.grades[grade].features) { + if (feature === 'shieldboost') { + blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1; + blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1; + } + } + } + } + } + return blueprint; +} diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js index 6c6a2272..47a1db01 100644 --- a/src/app/utils/CompanionApiUtils.js +++ b/src/app/utils/CompanionApiUtils.js @@ -2,6 +2,7 @@ import React from 'react'; import { Modifications, Modules, Ships } from 'coriolis-data/dist'; import Module from '../shipyard/Module'; import Ship from '../shipyard/Ship'; +import { getBlueprint } from '../utils/BlueprintFunctions'; // mapping from fd's ship model names to coriolis' const SHIP_FD_NAME_TO_CORIOLIS_NAME = { @@ -335,9 +336,9 @@ function _addModifications(module, modifiers, blueprint, grade) { } } - // Add the blueprint ID, grade and special + // Add the blueprint definition, grade and special if (blueprint) { - module.blueprint = Object.assign({}, Modifications.blueprints[blueprint]); + module.blueprint = getBlueprint(blueprint, module); if (grade) { module.blueprint.grade = Number(grade); } From 8ad9472d5612679b8466159a4e31af13add26a53 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 18:24:22 +0000 Subject: [PATCH 50/53] Provide correct blueprints limits for HRPs --- src/app/utils/BlueprintFunctions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index 0389f2cf..8c9f117d 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -81,8 +81,8 @@ export function getBlueprint(name, module) { // Start with a copy of the blueprint const blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name])); if (module) { - if (module.grp === 'bh') { - // Bulkheads need to have their resistances altered + if (module.grp === 'bh' || module.grp === 'hr') { + // Bulkheads and hull reinforcements need to have their resistances altered by the base values for (const grade in blueprint.grades) { for (const feature in blueprint.grades[grade].features) { if (feature === 'explres') { From b055963fe095a29dccb58d4aac03121caeb07dff Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 18:34:57 +0000 Subject: [PATCH 51/53] Fixes --- src/app/utils/BlueprintFunctions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index 8c9f117d..dcaf9555 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -30,7 +30,7 @@ export function blueprintTooltip(translate, features, m) if (m) { // We have a module - add in the current value let current = m.getModValue(feature); - if (featureDef.type === 'percentage') { + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { current = Math.round(current / 10) / 10; } results.push({translate(feature)}{lowerBound}{symbol}{current}{symbol}{upperBound}{symbol}); From 94d876e934de89ae197bd3eafb2bc731d7c18246 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Wed, 22 Mar 2017 21:38:33 +0000 Subject: [PATCH 52/53] Per-cell colour coding for blueprint tooltips --- src/app/utils/BlueprintFunctions.js | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index dcaf9555..7dcddf04 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -26,17 +26,32 @@ export function blueprintTooltip(translate, features, m) lowerBound = Math.round(lowerBound * 1000) / 10; upperBound = Math.round(upperBound * 1000) / 10; } - const range = `${lowerBound}${symbol} - ${upperBound}${symbol}`; + const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); + const upperIsBeneficial = isValueBeneficial(feature, upperBound); if (m) { // We have a module - add in the current value let current = m.getModValue(feature); if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { current = Math.round(current / 10) / 10; } - results.push({translate(feature)}{lowerBound}{symbol}{current}{symbol}{upperBound}{symbol}); + const currentIsBeneficial = isValueBeneficial(feature, current); + results.push( + + {translate(feature)} + {lowerBound}{symbol} + {current}{symbol} + {upperBound}{symbol} + + ); } else { // We do not have a module, no value - results.push({translate(feature)}{lowerBound}{symbol}{upperBound}{symbol}); + results.push( + + {translate(feature)} + {lowerBound}{symbol} + {upperBound}{symbol} + + ); } } } @@ -71,6 +86,18 @@ export function isBeneficial(feature, values) { } } +/** + * Is this feature value beneficial? + * + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + /** * Get a blueprint with a given name and an optional module * @param {string} name The name of the blueprint From 873935c1d497dea3b838c839a48e0f27a535807b Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Thu, 23 Mar 2017 08:17:07 +0000 Subject: [PATCH 53/53] Update to documentation --- src/app/components/EngineProfile.jsx | 1 + src/app/components/FSDProfile.jsx | 1 + src/app/components/LineChart.jsx | 8 +- src/app/components/Offence.jsx | 2 +- src/app/components/OutfittingSubpages.jsx | 4 +- src/app/components/WeaponDamageChart.jsx | 8 +- src/app/i18n/en.js | 113 ++++++++++++++-------- 7 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/app/components/EngineProfile.jsx b/src/app/components/EngineProfile.jsx index 2faff719..8c9ddf0f 100644 --- a/src/app/components/EngineProfile.jsx +++ b/src/app/components/EngineProfile.jsx @@ -99,6 +99,7 @@ export default class EngineProfile extends TranslatedComponent { func={this.state.calcMaxSpeedFunc} points={1000} code={code} + aspect={0.7} /> ); } diff --git a/src/app/components/FSDProfile.jsx b/src/app/components/FSDProfile.jsx index c5fea602..81548632 100644 --- a/src/app/components/FSDProfile.jsx +++ b/src/app/components/FSDProfile.jsx @@ -97,6 +97,7 @@ export default class FSDProfile extends TranslatedComponent { func={this.state.calcMaxRangeFunc} points={200} code={code} + aspect={0.7} /> ); } diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index 24edc448..33550cc6 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -15,7 +15,8 @@ export default class LineChart extends TranslatedComponent { xMin: 0, yMin: 0, points: 20, - colors: ['#ff8c0d'] + colors: ['#ff8c0d'], + aspect: 0.5 }; static propTypes = { @@ -32,6 +33,7 @@ export default class LineChart extends TranslatedComponent { series: React.PropTypes.array, colors: React.PropTypes.array, points: React.PropTypes.number, + aspect: React.PropTypes.number, code: React.PropTypes.string, }; @@ -123,7 +125,7 @@ export default class LineChart extends TranslatedComponent { const { xMax, xMin, yMin, yMax } = props; const { width, height } = this.state.dimensions; const innerWidth = width - MARGIN.left - MARGIN.right; - const outerHeight = Math.round(width * 2 / 3); // TODO make this an aspect property + const outerHeight = Math.round(width * props.aspect); const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); @@ -232,7 +234,7 @@ export default class LineChart extends TranslatedComponent { const lines = seriesLines.map((line, i) => ).reverse(); const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; - const xmark = xMark ? : ''; + const xmark = xMark ? : ''; return ( { this.setState({ dimensions }); }}> diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 0faa5c82..4f502c0f 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -171,7 +171,7 @@ export default class Offence extends TranslatedComponent { const effectivenessShieldsTooltipDetails = []; effectivenessShieldsTooltipDetails.push(
    {translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}
    ); effectivenessShieldsTooltipDetails.push(
    {translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}
    ); - effectivenessShieldsTooltipDetails.push(
    {translate('sys') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
    ); + effectivenessShieldsTooltipDetails.push(
    {translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
    ); const effectiveShieldsSDpsTooltipDetails = []; if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(
    {translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}
    ); diff --git a/src/app/components/OutfittingSubpages.jsx b/src/app/components/OutfittingSubpages.jsx index 5eb2abb1..2d25037c 100644 --- a/src/app/components/OutfittingSubpages.jsx +++ b/src/app/components/OutfittingSubpages.jsx @@ -111,12 +111,12 @@ export default class OutfittingSubpages extends TranslatedComponent {

    {translate('damage to opponent\'s shields')}

    - +

    {translate('damage to opponent\'s hull')}

    - +
    ; } diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx index dfb7f237..9c04fbef 100644 --- a/src/app/components/WeaponDamageChart.jsx +++ b/src/app/components/WeaponDamageChart.jsx @@ -20,6 +20,7 @@ export default class WeaponDamageChart extends TranslatedComponent { opponent: React.PropTypes.object.isRequired, hull: React.PropTypes.bool.isRequired, engagementRange: React.PropTypes.number.isRequired, + opponentSys: React.PropTypes.number.isRequired, marker: React.PropTypes.string.isRequired }; @@ -37,7 +38,7 @@ export default class WeaponDamageChart extends TranslatedComponent { */ componentWillMount() { const weaponNames = this._weaponNames(this.props.ship, this.context); - const opponentShields = Calc.shieldMetrics(this.props.opponent, 4); + const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys); const opponentArmour = Calc.armourMetrics(this.props.opponent); const maxRange = this._calcMaxRange(this.props.ship); const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour); @@ -54,7 +55,7 @@ export default class WeaponDamageChart extends TranslatedComponent { componentWillReceiveProps(nextProps, nextContext) { if (nextProps.marker != this.props.marker) { const weaponNames = this._weaponNames(nextProps.ship, nextContext); - const opponentShields = Calc.shieldMetrics(nextProps.opponent, 4); + const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys); const opponentArmour = Calc.armourMetrics(nextProps.opponent); const maxRange = this._calcMaxRange(nextProps.ship); const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour); @@ -175,7 +176,7 @@ export default class WeaponDamageChart extends TranslatedComponent { const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { formats, translate, units } = language; const { maxRange } = this.state; - const { ship, opponent } = this.props; + const { ship, opponent, engagementRange } = this.props; const sortOrder = this._sortOrder; const onCollapseExpand = this._onCollapseExpand; @@ -191,6 +192,7 @@ export default class WeaponDamageChart extends TranslatedComponent { xUnit={translate('m')} yLabel={translate('sdps')} series={this.state.weaponNames} + xMark={this.props.engagementRange} colors={DAMAGE_DEALT_COLORS} func={this.state.calcSDpsFunc} points={200} diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 36fd123e..5c68a1f8 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -299,7 +299,7 @@ Along the top of the screen are some of the key values for your build. This is Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.

    -All values are the highest possible, assuming that you have maximum pips in the relevant capacitor (ENG for speed, WEP for time to drain, etc.).

    +All values are the highest possible, assuming that you an optimal setup for that particular value (maximum pips in ENG for speed, minimum fuel for jump range, etc.). Details of the specific setup for each value are listed in the associated tootip.

    Modules

    The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions).

    @@ -312,30 +312,39 @@ To move a module from one slot to another drag it. If you instead want to copy Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers).

    -

    Offence Summary

    -The offence summary panel provides information about the damage that you deal with your weapons.

    +

    Ship Controls

    +The ship controls allow you to set your pips, boost, and amount of fuel and cargo that your build carries. The changes made here will effect the information supplied in the subsequent panels, giving you a clearer view of what effect different changing these items will have.

    -The first headline gives an overall damage per second rating: this is the optimum amount of damage the build will do per second according to weapon statistics. After that is a breakdown of the damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +Ship control settings are saved as part of a build.

    -The next headline gives an overall sustained damage per second rating: this is the optimum amount of damage the build will do per second over a longer period of time, taking in to account ammunition clip capacities and reload times. After that is a breakdown of the sustained damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +

    Opponent

    +The opponet selection allows you to choose your opponent. The opponent can be either a stock build of a ship or one of your own saved builds. You can also set the engagement range between you and your opponent. Your selection here will effect the information supplied in the subsequent panels, specifically the Offence and Defence panels.

    -The final headline gives an overall damage per energy rating: this is the amount of damage the build will do per unit of weapon capacitor energy expended. After that is a breakdown of the damage per energy the build will do for each type of damage: absolute, explosive, kinetic, and thermal.

    +Opponent settings are saved as part of a build.

    -

    Defence Summary

    -The defence summary panel provides information about the strength of your defences and the damage that you receive from opponents.

    +

    Power and Costs Sub-panels

    +

    Power

    +The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module. Disabled modules will not be included in the build's statistics, with the exception of Shield Cell Banks as they are usually disabled when not in use and only enabled when required.

    -The first headline gives your total shield strength (if you have shields), taking in to account your base shield plus boosters. After that are the details of how long it will take for your shields to recover from 0 to 50% (recovery time) and from 50% to 100% (recharge time). The next line provides a breakdown of the shield damage taken from different damage types. For example, if you damage from kinetic is 60% then it means that a weapon usually dealing 10 points of damage will only deal 6, the rest being resisted by the shield. Note that this does not include any resistance alterations due to pips in your SYS capacitor.

    +

    Costs

    +The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    -The second headline gives your total shield cell strength (if you have shield cell banks). This is the sum of the recharge of all of equipped shield cell banks.

    +The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    -The third headline gives your total armour strength, taking in to account your base armour plus hull reinforcement packages. The next line provides a breakdown of the hull damage taken from different damage types. For example, if you damage from kinetic is 120% then it means that a weapon usually dealing 10 points of damage will deal 12.

    +The reload costs provides information about the costs of reloading your current build.

    -The fourth headline gives your total module protection strength from module reinforcement packages. The next line provides a breakdown of the protection for both internal and external modules whilst all module reinforcement packages are functioning. For example, if external module protection is 20% then 10 points of damage will 2 points of damage to the module reinforcement packages and 8 points of damage to the module

    +

    Profiles

    +Profiles provide graphs that show the general performance of modules in your build -

    Movement Summary

    -The movement summary panel provides information about the build's speed and agility.

    +

    Engine Profile

    +The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed alters with the overall mass of your build. The vertical dashed line on the graph shows your current mass. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your maximum speed by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your speed by hitting the boost button.

    + +

    FSD Profile

    +The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The vertical dashed line on the graph shows your current maximum single jump range. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD, and you can increase your maximum jump range by reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build,

    + +

    Movement Profile

    +The movement profile panel provides information about the capabilities of your current thrusters with your current overall mass and ENG pips settings. The diagram shows your ability to move and rotate in the different axes: -Along the top of this panel are the number of pips you put in to your ENG capacitor, from 0 to 4 and also include 4 pips and boost (4b). Along the side of this panel are the names of the metrics. These are:
    Speed
    The fastest the ship can move, in metres per second
    Pitch
    The fastest the ship can raise or lower its nose, in degrees per second
    @@ -343,44 +352,72 @@ Along the top of this panel are the number of pips you put in to your ENG capaci
    Yaw
    The fastest the ship can turn its nose left or right, in degrees per second
    -

    Power Management

    -The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module.

    +Your movement profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your movement values by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your movement profile by hitting the boost button.

    -

    Costs

    -The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the 'Settings' at the top-right of the page.

    +

    Damage Profile

    +The damage profile provides two graphs showing how the the build's damage to the opponent's shields and hull change with engagement range. The vertical dashed line on the graph shows your current engagement range. This combines information about the build's weapons with the opponent's shields and hull to provide an accurate picture of sustained damage that can be inflicted on the opponent.

    -The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.

    +

    Offence

    +

    Summary

    +The offence summary provides per-weapon information about sustained damage per second inflicted to shields and hull, along with a measure of effectiveness of that weapon. The effectiveness value has a tooltip that provides a breakdown of the effectiveness, and can include reductions or increases due to range, resistance, and either power distributor (for shields) or hardness (for hull). The final effectiveness value is calculated by multiplying these percentages together.

    -The reload costs provides information about the costs of reloading your current build.

    +

    Offence Metrics

    +The offence metrics panel provides information about your offence.

    -

    Engine Profile

    -The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed (with a full fuel tank and 4 pips to engines) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters.

    +Time to drain is a measure of how quickly your WEP capacitor will drain when firing all weapons. It is affected by the number of pips you have in your WEP capacitor, with more pips resulting in a higher WEP recharge rate and hence a longer time to drain.

    -

    FSD Profile

    -The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range (with a full fuel tank) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD.

    +The next value is the time it will take you to remove your opponent's shields. This assumes that you have 100% time on target and that your engagement range stays constant. Note that if your time to remove shields is longer than your time to drain this assumes that you continue firing throughout, inflicting lower damage due to the reduced energy in your WEP capacitor.

    -

    Jump Range

    -The jump range panel provides information about the build' jump range. The graph shows how the build's jump range changes with the amount of cargo on-board. The slider can be altered to change the amount of fuel you have on-board.

    +The next value is the time it will take you to remove your opponent's armour. This follows the same logic as the time to remove shields.

    -

    Damage Dealt

    -The damage dealt panel provides information about the effectiveness of your build's weapons against opponents' shields and hull at different engagement distances.

    +

    Shield Damage Sources

    +The shield damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    -The ship against which you want to check damage dealt can be selected by clicking on the red ship icon or the red ship name at the top of this panel.

    +

    Hull Damage Sources

    +The hull damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided.

    -The main section of this panel is a table showing your weapons and their effectiveness. Effectiveness against shields takes in to account the weapon and its engagement range, and assumes standard shield resistances. Effectiveness against hull takes in to account the weapon and, its engagement range and the target's hardness, and assumes military grade armour resistances.

    +

    Defence

    +

    Shield Metrics

    +The shield metrics provides information about your shield defence.

    -Effective DPS and effective SDPS are the equivalent of DPS and SDPS for the weapon. Effectiveness is a percentage value that shows how effective the DPS of the weapon is compared in reality against the given target compared to the weapon's stated DPS. Effectiveness can never go above 100%.

    +Raw shield strength is the sum of the shield from your generator, boosters and shield cell banks. A tooltip provides a breakdown of these values.

    -Total effective DPS, SDPS and effectiveness against both shields and hull are provided at the bottom of the table.

    +The time the shields will hold for is the time it will take your opponent' to remove your shields. This assumes that they have 100% time on target and that the engagement range stays constant. It also assumes that you fire all of your shield cell banks prior to your shields being lost.

    -At the bottom of this panel you can change your engagement range. The engagement range is the distance between your ship and your target. Many weapons suffer from what is known as damage falloff, where their effectiveness decreases the further the distance between your ship and your target. This allows you to model the effect of engaging at different ranges. +The time the shields will recover in is the time it will take your shields to go from collapsed (0%) to recovered (50%). This is affected by the number of pips you have in your SYS capacitor.

    -Note that this panel only shows enabled weapons, so if you want to see your overall effectiveness for a subset of your weapons you can disable the undesired weapons in the power management panel.

    +The time the shields will recharge in is the time it will take your shields to go from recovered (50%) to full (100%). This is affected by the number of pips you have in your SYS capacitor.

    -At the bottom of this panel are two graphs showing how your sustained DPS changes with engagement range. This shows at a glance how effective each weapon is at different distances.

    +Shield Sources +This chart provides information about the sources of your shields. For each applicable source of shields (generator, boosters, shield cell banks) a value is provided.

    -

    Damage Received

    -The damage received panel provides information about the effectiveness of your build's defences against opponent's weapons at different engagement range. Features and functions are the same as the damage dealt panel, except that it does take in to account your build's resistances.

    +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the shields. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    + +Effective Shield +This graph shows the effective shield for each damage type, found by dividing the raw shield value by the damage taken for that type.

    + +

    Amour Metrics

    +The armour metrics provides information about your armour defence.

    + +Raw armour strength is the sum of the armour from your bulkheads and hull reinforcement packages. A tooltip provides a breakdown of these values.

    + +The time the armour will hold for is the time it will take your opponent' to take your armour to 0. This assumes that they have 100% time on target, the engagement range stays constant, and that all damage is dealt to the armour rather than modules.

    + +Raw module armour is the sum of the protection from your module reinforcement packages.

    + +Protection for hardpoints is the amount of protection that your module reinforcement packages provide to hardpoints. This percentage of damage to the hardpoints will be diverted to the module reinforcement packages.

    + +Protection for all other modules is the amount of protection that your module reinforcement packages provide to everything other than hardpoints. This percentage of damage to the modules will be diverted to the module reinforcement packages.

    + +Armour Sources +This chart provides information about the sources of your armour. For each applicable source of shields (bulkheads, hull reinforcement packages) a value is provided.

    + +Damage Taken +This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the armour. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values.

    + +Effective Armour +This graph shows the effective armour for each damage type, found by dividing the raw armour value by the damage taken for that type.

    Keyboard Shortcuts