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

- -
- */} + + + + + + + + + + + + + + + + {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; +}