From 8c267150a9aca54fc22fb1f534cf453ad1fdb39a Mon Sep 17 00:00:00 2001 From: felixlinker Date: Thu, 13 Aug 2020 20:20:22 +0200 Subject: [PATCH] Rework Offence tab --- src/app/components/Offence.jsx | 490 ++++++++++++++++++--------------- 1 file changed, 275 insertions(+), 215 deletions(-) diff --git a/src/app/components/Offence.jsx b/src/app/components/Offence.jsx index 0bc4eee2..1ef563c4 100644 --- a/src/app/components/Offence.jsx +++ b/src/app/components/Offence.jsx @@ -3,102 +3,36 @@ import PropTypes from 'prop-types'; 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 { Ship } from 'ed-forge'; import autoBind from 'auto-bind'; +import { DAMAGE_METRICS } from 'ed-forge/lib/ship-stats'; +import { clone, mapValues, mergeWith, reverse, sortBy, sum, toPairs, values } from 'lodash'; /** - * 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 + * Turns an object into a tooltip. + * @param {function} translate Translate function + * @param {object} o Map to make the tooltip from + * @returns {React.Component} Tooltip */ -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); - }; -} - -/** - * Creates a tooltip that shows damage by type. - * @param {function} translate Translation function - * @param {Object} formats Object that holds format functions - * @param {Calc.SDps} sdpsObject Object that holds sdps split up by type - * @returns {Array} Tooltip - */ -function getSDpsTooltip(translate, formats, sdpsObject) { - return Object.keys(sdpsObject).filter(key => sdpsObject[key]) - .map(key => { - return ( -
- {translate(key) + ' ' + formats.f1(sdpsObject[key])} -
- ); - }); +function objToTooltip(translate, o) { + return toPairs(o) + .filter(([k, v]) => Boolean(v)) + .map(([k, v]) =>
{`${translate(k)}: ${v}`}
); } /** * Returns a data object used by {@link PieChart} that shows damage by type. - * @param {function} translate Translation function - * @param {Calc.SDps} sdpsObject Object that holds sdps split up by type - * @returns {Object} Data object + * @param {function} translate Translation function + * @param {Calc.SDps} o Object that holds sdps split up by type + * @returns {Object} Data object */ -function getSDpsData(translate, sdpsObject) { - return Object.keys(sdpsObject).map(key => { - return { - value: Math.round(sdpsObject[key]), - label: translate(key) - }; +function objToPie(translate, o) { + return toPairs(o).map(([k, value]) => { + return { label: translate(k), value }; }); } -/** - * Adds all damage of `add` onto `addOn`. - * @param {Calc.SDps} addOn Object that holds sdps split up by type (will be mutated) - * @param {Calc.SDps} add Object that holds sdps split up by type - */ -function addSDps(addOn, add) { - Object.keys(addOn).map(k => addOn[k] += (add[k] ? add[k] : 0)); -} - -/** - * Calculates the overall sdps of an sdps object. - * @param {Calc.SDps} sdpsObject Object that holds sdps spluit up by type - */ -function sumSDps(sdpsObject) { - if (sdpsObject.total) { - return sdpsObject.total; - } - - return Object.keys(sdpsObject).reduce( - (acc, k) => acc + (sdpsObject[k] ? sdpsObject[k] : 0), - 0 - ); -} - /** * Offence information * Offence information consists of four panels: @@ -109,12 +43,10 @@ function sumSDps(sdpsObject) { */ export default class Offence extends TranslatedComponent { static propTypes = { - marker: PropTypes.string.isRequired, - ship: PropTypes.object.isRequired, - opponent: PropTypes.object.isRequired, - engagementrange: PropTypes.number.isRequired, - wep: PropTypes.number.isRequired, - opponentSys: PropTypes.number.isRequired + code: PropTypes.string.isRequired, + ship: PropTypes.instanceOf(Ship).isRequired, + opponent: PropTypes.instanceOf(Ship).isRequired, + engagementRange: PropTypes.number.isRequired, }; /** @@ -123,146 +55,196 @@ export default class Offence extends TranslatedComponent { */ constructor(props) { super(props); + autoBind(this); - this._sort = this._sort.bind(this); - - const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange); this.state = { - predicate: 'n', + predicate: 'classRating', desc: true, - damage }; } - /** - * 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.eng != nextProps.eng) { - const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, 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(predicate, desc); + let desc = predicate == this.state.predicate ? !this.state.desc : true; this.setState({ predicate, desc }); } - /** - * Sorts the weapon list - * @param {string} predicate Sort predicate - * @param {Boolean} desc Sort order descending - */ - _sort(predicate, desc) { - let comp = weaponComparator.bind(null, this.context.language.translate); - - switch (predicate) { - case 'n': comp = comp(null, 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); - } - /** * Render offence * @return {React.Component} contents */ render() { - const { ship, opponent, wep, engagementrange } = this.props; + const { ship } = this.props; const { language, tooltip, termtip } = this.context; const { formats, translate, units } = language; - const { damage } = this.state; const sortOrder = this._sortOrder; - const pd = ship.standard[4].m; + const damage = ship.getMetrics(DAMAGE_METRICS); + const portions = { + Absolute: damage.types.abs, + Explosive: damage.types.expl, + Kinetic: damage.types.kin, + Thermic: damage.types.therm, + }; - const opponentShields = Calc.shieldMetrics(opponent, 4); - const opponentArmour = Calc.armourMetrics(opponent); + const oppShield = ship.getOpponent().getShield(); + const shieldMults = { + Absolute: 1, + Explosive: oppShield.explosive.damageMultiplier, + Kinetic: oppShield.kinetic.damageMultiplier, + Thermic: oppShield.thermal.damageMultiplier, + }; - const timeToDrain = Calc.timeToDrainWep(ship, wep); + const oppArmour = ship.getOpponent().getArmour(); + const armourMults = { + Absolute: 1, + Explosive: oppArmour.explosive.damageMultiplier, + Kinetic: oppArmour.kinetic.damageMultiplier, + Thermic: oppArmour.thermal.damageMultiplier, + }; + let rows = []; + for (let weapon of ship.getHardpoints()) { + const sdps = weapon.get('sustaineddamagepersecond'); + const byRange = weapon.getRangeEffectiveness(); + const weaponPortions = { + Absolute: weapon.get('absolutedamageportion'), + Explosive: weapon.get('explosivedamageportion'), + Kinetic: weapon.get('kineticdamageportion'), + Thermic: weapon.get('thermicdamageportion'), + }; + const baseSdpsTooltip = objToTooltip( + translate, + mapValues(weaponPortions, (p) => formats.f1(sdps * p)), + ); - let totalSEps = 0; - let totalSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 }; - let shieldsSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 }; - let armourSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 }; + const bySys = oppShield.absolute.bySys; + const shieldResEfts = mergeWith( + clone(weaponPortions), + shieldMults, + (objV, srcV) => objV * srcV + ); + const byShieldRes = sum(values(shieldResEfts)); + const shieldsSdpsTooltip = objToTooltip( + translate, + mapValues( + shieldResEfts, + (mult) => formats.f1(byRange * mult * bySys * sdps), + ), + ); + const shieldsEftTooltip = objToTooltip( + translate, + { + range: formats.pct1(byRange), + resistance: formats.pct1(byShieldRes), + 'power distributor': formats.pct1(bySys), + }, + ); + const shieldEft = byRange * byShieldRes * bySys; - const rows = []; - for (let i = 0; i < damage.length; i++) { - const weapon = damage[i]; + const byHardness = weapon.getArmourEffectiveness(); + const armourResEfts = mergeWith( + clone(weaponPortions), + armourMults, + (objV, srcV) => objV * srcV, + ); + const byArmourRes = sum(values(armourResEfts)); + const armourSdpsTooltip = objToTooltip( + translate, + mapValues( + armourResEfts, + (mult) => formats.f1(byRange * mult * byHardness * sdps) + ), + ); + const armourEftTooltip = objToTooltip( + translate, + { + range: formats.pct1(byRange), + resistance: formats.pct1(byArmourRes), + hardness: formats.pct1(byHardness), + }, + ); + const armourEft = byRange * byArmourRes * byHardness; - totalSEps += weapon.seps; - addSDps(totalSDpsObject, weapon.sdps.base); - addSDps(shieldsSDpsObject, weapon.sdps.shields); - addSDps(armourSDpsObject, weapon.sdps.armour); - - const baseSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.base); - - 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('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}
); - - const effectiveShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour); - - 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 = getSDpsTooltip(translate, formats, weapon.sdps.armour); - - 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.base.total)} - {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 bp = weapon.getBlueprint(); + const grade = weapon.getBlueprintGrade(); + const exp = weapon.getExperimental(); + let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`; + if (exp) { + bpTitle += `, ${translate(exp)}`; + } + rows.push({ + slot: weapon.getSlot(), + mount: weapon.mount, + classRating: weapon.getClassRating(), + type: weapon.readMeta('type'), + bpTitle: bp ? ` (${bpTitle})` : null, + sdps, + baseSdpsTooltip, + shieldSdps: sdps * shieldEft, + shieldEft, + shieldsSdpsTooltip, + shieldsEftTooltip, + armourSdps: sdps * armourEft, + armourEft, + armourSdpsTooltip, + armourEftTooltip, + }); + } + const { predicate, desc } = this.state; + rows = sortBy(rows, (row) => row[predicate]); + if (desc) { + reverse(rows); } - const totalSDps = sumSDps(totalSDpsObject); - const totalSDpsTooltipDetails = getSDpsTooltip(translate, formats, totalSDpsObject); - const totalSDpsData = getSDpsData(translate, totalSDpsObject); + const sdpsTooltip = objToTooltip( + translate, + mapValues(portions, (p) => formats.f1(damage.sustained.dps * p)), + ); + const sdpsPie = objToPie( + translate, + mapValues(portions, (p) => Math.round(damage.sustained.dps * p)), + ); - const totalShieldsSDps = sumSDps(shieldsSDpsObject); - const totalShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, shieldsSDpsObject); - const shieldsSDpsData = getSDpsData(translate, shieldsSDpsObject); + const shieldSdpsSrcs = mergeWith( + clone(portions), + shieldMults, + (objV, srcV) => damage.sustained.dps * oppShield.absolute.bySys * + damage.rangeMultiplier * objV * srcV, + ); + const shieldsSdps = sum(values(shieldSdpsSrcs)); + const shieldsSdpsTooltip = objToTooltip( + translate, + mapValues(shieldSdpsSrcs, (v) => formats.f1(v)), + ); + const shieldsSdpsPie = objToPie( + translate, + mapValues(shieldSdpsSrcs, (v) => Math.round(v)), + ); - const totalArmourSDps = sumSDps(armourSDpsObject); - const totalArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, armourSDpsObject); - const armourSDpsData = getSDpsData(translate, armourSDpsObject); + const armourSdpsSrcs = mergeWith( + clone(portions), + armourMults, + (objV, srcV) => damage.sustained.dps * damage.hardnessMultiplier * + damage.rangeMultiplier * objV * srcV, + ); + const armourSdps = sum(values(armourSdpsSrcs)); + const totalArmourSDpsTooltipDetails = objToTooltip( + translate, + mapValues(armourSdpsSrcs, (v) => formats.f1(v)), + ); + const armourSDpsData = objToPie( + translate, + mapValues(armourSdpsSrcs, (v) => Math.round(v)), + ); - 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)); + const pd = ship.getPowerDistributor(); + const timeToDrain = damage.sustained.timeToDrain[ship.getDistributorSettings().Wep]; + // const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, shieldsSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); + // const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, armourSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); return ( @@ -270,28 +252,75 @@ export default class Offence extends TranslatedComponent { - + - - - - - + + + + + - {rows} + {rows.map((row) => ( + + + + + + + + + ))} {rows.length > 0 && - - + + - + } @@ -300,22 +329,53 @@ export default class Offence extends TranslatedComponent {

{translate('offence metrics')}

-

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

-

{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}
{formats.f1(totalShieldsSDps)}

-

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

-

{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}
{formats.f1(totalArmourSDps)}

-

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

+

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

+

+ {translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}
+ {formats.f1(shieldsSdps)} +

+

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

+

+ {translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}
+ {formats.f1(armourSdps)} +

+

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

-

{translate('overall damage')}

- +

+ {translate('overall damage')} +

+
-

{translate('shield damage sources')}

- +

+ {translate('shield damage sources')} +

+
-

{translate('armour damage sources')}

+

+ {translate('armour damage sources')} +

);
{translate('weapon')}{translate('weapon')} {translate('overall')} {translate('opponent\'s shields')} {translate('opponent\'s armour')}
{'sdps'}{'sdps'}{'eft'}{'sdps'}{'eft'}sdpssdpseftsdpseft
+ {row.mount == 'F' ? : null} + {row.mount == 'G' ? : null} + {row.mount == 'T' ? : null} + {row.classRating} {translate(row.type)} + {row.bpTitle} + + {formats.f1(row.sdps)} + {formats.f1(row.shieldSdps)} + {formats.pct1(row.shieldEft)} + {formats.f1(row.armourSdps)} + {formats.pct1(row.armourEft)}
={formats.f1(totalSDps)}={formats.f1(totalShieldsSDps)} + + ={formats.f1(damage.sustained.dps)} + + + + ={formats.f1(shieldsSdps)} + + ={formats.f1(totalArmourSDps)} + + ={formats.f1(armourSdps)} + +