diff --git a/src/app/components/WeaponDamageChart.jsx b/src/app/components/WeaponDamageChart.jsx index cf4ab888..cdafaa1d 100644 --- a/src/app/components/WeaponDamageChart.jsx +++ b/src/app/components/WeaponDamageChart.jsx @@ -3,193 +3,99 @@ import PropTypes from 'prop-types'; import TranslatedComponent from './TranslatedComponent'; import LineChart from '../components/LineChart'; import * as Calc from '../shipyard/Calculations'; +import { moduleReduce } from 'ed-forge/lib/helper'; +import { chain, keys, mapValues, values } from 'lodash'; const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777']; +const PORTION_MAPPINGS = { + 'absolute': 'absolutedamageportion', + 'explosive': 'explosivedamageportion', + 'kinetic': 'kineticdamageportion', + 'thermal': 'thermicdamageportion', +}; +const MULTS = keys(PORTION_MAPPINGS); + +// TODO: help with this in ed-forge +/** + * . + * @param {Object} opponentDefence . + * @returns {Object} . + */ +function defenceToMults(opponentDefence) { + return chain(opponentDefence) + .pick(MULTS) + .mapKeys((v, k) => PORTION_MAPPINGS[k]) + .mapValues((resistanceProfile) => resistanceProfile.damageMultiplier) + .value(); +} /** * Weapon damage chart */ export default class WeaponDamageChart extends TranslatedComponent { static propTypes = { + code: PropTypes.string.isRequired, ship: PropTypes.object.isRequired, - opponent: PropTypes.object.isRequired, - hull: PropTypes.bool.isRequired, + opponentDefence: PropTypes.object.isRequired, engagementRange: PropTypes.number.isRequired, - opponentSys: PropTypes.number.isRequired, - marker: PropTypes.string.isRequired }; - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - } - - /** - * Set the initial weapons state - */ - componentWillMount() { - const weaponNames = this._weaponNames(this.props.ship, this.context); - 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); - - this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, 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 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); - this.setState({ weaponNames, - opponentShields, - opponentArmour, - maxRange, - maxDps, - calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, 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 {Object} opponentShields The opponent's shields - * @param {Object} opponentArmour The opponent's armour - * @return {number} The maximum sustained single-weapon DPS - */ - _calcMaxSDps(ship, opponent, opponentShields, opponentArmour) { - // Additional information to allow effectiveness calculations - 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 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; - } - } - } - 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 {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, 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; - const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange); - results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total; - } - } - return results; - } - /** * 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, engagementRange } = this.props; + const { language } = this.context; + const { translate } = language; + const { code, ship, opponentDefence, engagementRange } = this.props; - const sortOrder = this._sortOrder; - const onCollapseExpand = this._onCollapseExpand; - - const code = `${ship.toString()}:${opponent.toString()}`; + const hardpoints = ship.getHardpoints(); + const hardpointsMap = chain(hardpoints) + .map((m) => [m.getSlot(), m]) + .fromPairs() + .value(); + const mults = defenceToMults(opponentDefence); + const cb = (range) => { + return mapValues( + hardpointsMap, + (m) => { + const sdps = m.get('sustaineddamagepersecond', true); + const resistanceMul = chain(mults) + .toPairs() + .map((pair) => { + const [k, mul] = pair; + return m.get(k, true) * mul; + }) + .sum() + .value(); + const falloff = m.get('damagefalloffrange', true); + const rangeMul = Math.min(1, Math.max(0, + 1 - (range - falloff) / (m.get('maximumrange', true) - falloff) + )); + return sdps * resistanceMul * rangeMul; + } + ); + }; return (
Math.max(a, v), 1000, + )} + yMin={0} + // Factor in highest damage multiplier to get a safe upper bound + yMax={Math.max(1, ...values(mults)) * moduleReduce( + hardpoints, 'sustaineddamagepersecond', true, (a, v) => Math.max(a, v), 0, + )} xLabel={translate('range')} xUnit={translate('m')} - yLabel={translate('sdps')} - series={this.state.weaponNames} - xMark={this.props.engagementRange} + yLabel={translate('sustaineddamagepersecond')} + series={hardpoints.map((m) => m.getSlot())} + xMark={engagementRange} colors={DAMAGE_DEALT_COLORS} - func={this.state.calcSDpsFunc} + func={cb} points={200} code={code} />