diff --git a/ChangeLog.md b/ChangeLog.md index 5f7d1c94..509edb97 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -14,6 +14,7 @@ * Version URLs to handle changes to ship specifications over time * Do not include disabled shield boosters in calculations * Add 'Damage dealt' section + * Add 'Damage received' section #2.2.5 * Calculate rate of fire for multi-burst weapons diff --git a/src/app/components/DamageDealt.jsx b/src/app/components/DamageDealt.jsx index 370dea4d..d4ce6b0a 100644 --- a/src/app/components/DamageDealt.jsx +++ b/src/app/components/DamageDealt.jsx @@ -1,9 +1,44 @@ import React from 'react'; -import cn from 'classnames'; import TranslatedComponent from './TranslatedComponent'; import { Ships } from 'coriolis-data/dist'; -import { slotName, slotComparator } from '../utils/SlotFunctions'; import ShipSelector from './ShipSelector'; +import { nameComparator } from '../utils/SlotFunctions'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; + +/** + * 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); + }; +} /** * Damage against a selected ship @@ -14,6 +49,8 @@ export default class DamageDealt extends TranslatedComponent { code: React.PropTypes.string.isRequired }; + static DEFAULT_AGAINST = Ships['anaconda']; + /** * Constructor * @param {Object} props React Component properties @@ -27,16 +64,63 @@ export default class DamageDealt extends TranslatedComponent { this.state = { predicate: 'n', desc: true, - against: Ships['anaconda'], + against: DamageDealt.DEFAULT_AGAINST }; } + /** + * Set the initial weapons state + */ + componentWillMount() { + const weapons = this._calcWeapons(this.props.ship, this.state.against); + this.setState({ weapons: weapons }); + } + + /** + * Set the updated weapons state + */ + componentWillReceiveProps(nextProps, nextContext) { + const weapons = this._calcWeapons(this.props.ship, this.state.against); + this.setState({ weapons: weapons }); + return true; + } + + _calcWeapons(ship, against) { + // Create a list of the ship's weapons and include required stats - this is so that we muck around with re-ordering and the like on the fly + let weapons = []; + + for (let i = 0; i < ship.hardpoints.length; i++) { + if (ship.hardpoints[i].m) { + const m = ship.hardpoints[i].m; + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + const effectiveness = m.getPiercing() >= against.properties.hardness ? 1 : m.getPiercing() / against.properties.hardness; + const effectiveDps = m.getDps() * effectiveness; + const effectiveSDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectiveness : effectiveDps; + + weapons.push({id: i, + mount: m.mount, + name: m.name || m.grp, + classRating: classRating, + effectiveDps: effectiveDps, + effectiveSDps: effectiveSDps, + effectiveness: effectiveness}); + } + } + + return weapons; + } + /** * Triggered when the ship changes * @param {string} s the new ship ID */ _onShipChange(s) { - this.setState({ against: Ships[s] }); + const against = Ships[s]; + const weapons = this._calcWeapons(this.props.ship, against); + // This is not the correct 'this' +console.log('1) State against is' + this.state.against.properties.name); + this.setState({ against: against, weapons: weapons }); +console.log('2) State against is' + this.state.against.properties.name); } /** @@ -59,53 +143,52 @@ export default class DamageDealt extends TranslatedComponent { /** * Sorts the weapon list * @param {Ship} ship Ship instance - * @param {Ship} against The ship to compare against * @param {string} predicate Sort predicate * @param {Boolean} desc Sort order descending */ - _sort(ship, against, predicate, desc) { - let weaponList = ship.hardpoints; - let comp = slotComparator.bind(null, this.context.language.translate); + _sort(ship, predicate, desc) { + let comp = weaponComparator.bind(null, this.context.language.translate); switch (predicate) { case 'n': comp = comp(null, desc); break; - case 'd': comp = comp((a, b) => a.m.getDps() - b.m.getDps(), desc); break; - case 'e': comp = comp((a, b) => (a.m.getPiercing() > a.m.hardness ? a.m.getDps() : a.m.getDps() * a.m.getPiercing() / a.m.hardness) - (b.m.getPiercing() > b.m.hardness ? b.m.getDps() : b.m.getDps() * b.m.getPiercing() / b.m.hardness), desc); break; + case 'edps': comp = comp((a, b) => a.effectiveDps - b.effectiveDps, desc); break; + case 'esdps': comp = comp((a, b) => a.effectiveSDps - b.effectiveSDps, desc); break; + case 'e': comp = comp((a, b) => a.effectiveness - b.effectiveness, desc); break; } - weaponList.sort(comp); + this.state.weapons.sort(comp); } /** * Render individual rows for hardpoints * @param {Function} translate Translate function * @param {Object} formats Localised formats map - * @param {Object} ship Our ship - * @param {Object} against The ship against which to compare * @return {array} The individual rows * */ - _renderRows(translate, formats, ship, against) { + _renderRows(translate, formats) { + const { termtip, tooltip } = this.context; + let rows = []; - for (let hardpoint in ship.hardpoints) { - if (ship.hardpoints[hardpoint].m) { - const m = ship.hardpoints[hardpoint].m; - const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; - const effectiveness = m.getPiercing() >= against.properties.hardness ? 1 : m.getPiercing() / against.properties.hardness; - const effectiveDps = m.getDps() * effectiveness; - const effectiveSDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) * effectiveness : effectiveDps; + if (this.state.weapons) { + for (let i = 0; i < this.state.weapons.length; i++) { + const weapon = this.state.weapons[i]; - rows.push( - {classRating} {slotName(translate, ship.hardpoints[hardpoint])} - {formats.round1(effectiveDps)} - {formats.round1(effectiveSDps)} - {formats.pct(effectiveness)} + rows.push( + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + + {formats.round1(weapon.effectiveDps)} + {formats.round1(weapon.effectiveSDps)} + {formats.pct(weapon.effectiveness)} ); } } - return rows; } @@ -117,25 +200,23 @@ export default class DamageDealt extends TranslatedComponent { const { language, tooltip, termtip } = this.context; const { formats, translate } = language; - const ship = this.props.ship; - const against = this.state.against; - const hardness = against.properties.hardness; + const sortOrder = this._sortOrder; return (

{translate('damage dealt against')}

- +
- - - - + + + + - {this._renderRows(translate, formats, ship, against)} + {this._renderRows(translate, formats)}
{translate('weapon')}{translate('effective dps')}{translate('effective sdps')}{translate('effectiveness')}{translate('weapon')}{translate('effective dps')}{translate('effective sdps')}{translate('effectiveness')}
diff --git a/src/app/components/DamageReceived.jsx b/src/app/components/DamageReceived.jsx new file mode 100644 index 00000000..32b703ad --- /dev/null +++ b/src/app/components/DamageReceived.jsx @@ -0,0 +1,259 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Modules } from 'coriolis-data/dist'; +import { nameComparator } from '../utils/SlotFunctions'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; + +/** + * 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); + }; +} + +/** + * Damage received by a selected ship + */ +export default class DamageReceived extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + + this._sort = this._sort.bind(this); + + this.state = { + predicate: 'n', + desc: true + }; + } + + /** + * Set the initial weapons state + */ + componentWillMount() { + this.setState({ weapons: this._calcWeapons(this.props.ship) }); + } + + /** + * Set the updated weapons state + */ + componentWillReceiveProps(nextProps, nextContext) { + this.setState({ weapons: this._calcWeapons(nextProps.ship) }); + return true; + } + + _calcWeapons(ship) { + // Create a list of all weapons and include their stats - this is so that we can muck around with re-ordering and the like on the fly + let weapons = []; + + for (let grp in Modules.hardpoints) { + if (Modules.hardpoints[grp][0].damage && Modules.hardpoints[grp][0].type) { + for (let mId in Modules.hardpoints[grp]) { + const m = Modules.hardpoints[grp][mId]; + const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; + + // Basic values + let damage = m.damage; + let rpshot = m.roundspershot || 1; + let rof = m.rof || 1; + + // Base DPS + const baseDps = damage * rpshot * rof; + const baseSDps = m.clip ? (m.clip * baseDps / m.rof) / ((m.clip / m.rof) + m.reload) : baseDps; + + // Effective DPS taking in to account shield resistance + let effectivenessShields = 0; + if (m.type.indexOf('E') != -1) { + effectivenessShields += ship.shieldExplRes; + } + if (m.type.indexOf('K') != -1) { + effectivenessShields += ship.shieldKinRes; + } + if (m.type.indexOf('T') != -1) { + effectivenessShields += ship.shieldThermRes; + } + effectivenessShields /= m.type.length; + // Plasma accelerators deal absolute damage + if (m.grp == 'pa') effectivenessShields = 1; + const effectiveDpsShields = baseDps * effectivenessShields; + const effectiveSDpsShields = baseSDps * effectivenessShields; + + // Effective DPS taking in to account hull hardness and resistance + let effectivenessHull = 0; + if (m.type.indexOf('E') != -1) { + effectivenessHull += ship.hullExplRes; + } + if (m.type.indexOf('K') != -1) { + effectivenessHull += ship.hullKinRes; + } + if (m.type.indexOf('T') != -1) { + effectivenessHull += ship.hullThermRes; + } + effectivenessHull /= m.type.length; + // Plasma accelerators deal absolute damage (but could be reduced by hardness) + if (m.grp == 'pa') effectivenessHull = 1; + effectivenessHull *= Math.min(m.piercing / ship.hardness, 1); + const effectiveDpsHull = baseDps * effectivenessHull; + const effectiveSDpsHull = baseSDps * effectivenessHull; + + weapons.push({id: m.id, + classRating: classRating, + name: m.name || m.grp, + mount: m.mount, + effectiveDpsShields: effectiveDpsShields, + effectiveSDpsShields: effectiveSDpsShields, + effectivenessShields: effectivenessShields, + effectiveDpsHull: effectiveDpsHull, + effectiveSDpsHull: effectiveSDpsHull, + effectivenessHull: effectivenessHull}); + } + } + } + + return weapons; + } + + /** + * 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.weapons.sort(comp); + } + + /** + * Render individual rows for weapons + * @param {Function} translate Translate function + * @param {Object} formats Localised formats map + * @return {array} The individual rows + * + */ + _renderRows(translate, formats) { + const { termtip, tooltip } = this.context; + + let rows = []; + + for (let i = 0; i < this.state.weapons.length; i++) { + const weapon = this.state.weapons[i]; + rows.push( + + {weapon.mount == 'F' ? : null} + {weapon.mount == 'G' ? : null} + {weapon.mount == 'T' ? : null} + {weapon.classRating} {translate(weapon.name)} + + {formats.round1(weapon.effectiveDpsShields)} + {formats.round1(weapon.effectiveSDpsShields)} + {formats.pct(weapon.effectivenessShields)} + {formats.round1(weapon.effectiveDpsHull)} + {formats.round1(weapon.effectiveSDpsHull)} + {formats.pct(weapon.effectivenessHull)} + ); + } + return rows; + } + + /** + * Render damage received + * @return {React.Component} contents + */ + render() { + const { language, tooltip, termtip } = this.context; + const { formats, translate } = language; + + const sortOrder = this._sortOrder; + + return ( + +

{translate('damage received by')}

+ + + + + + + + + + + + + + + + + + {this._renderRows(translate, formats)} + +
{translate('weapon')}{translate('against shields')}{translate('against hull')}
{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}{translate('DPS')}{translate('SDPS')}{translate('effectiveness')}
+
+ ); + } +} diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index df51dc92..6ffbe2ab 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -90,6 +90,9 @@ export const terms = { // Notification of restricted slot emptyrestricted: 'empty (restricted)', 'damage dealt against': 'Damage dealt against', + 'damage received by': 'Damage received by', + 'against shields': 'Against shields', + 'against hull': 'Against hull', // 'ammo' was overloaded for outfitting page and modul info, so changed to ammunition for outfitting page ammunition: 'Ammo', diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 4473e57b..847eeb35 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -18,6 +18,7 @@ import OffenceSummary from '../components/OffenceSummary'; import DefenceSummary from '../components/DefenceSummary'; import MovementSummary from '../components/MovementSummary'; import DamageDealt from '../components/DamageDealt'; +import DamageReceived from '../components/DamageReceived'; import LineChart from '../components/LineChart'; import PowerManagement from '../components/PowerManagement'; import CostSection from '../components/CostSection'; @@ -353,6 +354,10 @@ export default class OutfittingPage extends Page { +
+ +
+ ); } diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 0f912e50..efcd7c9b 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -5,7 +5,7 @@ import Module from './Module'; import LZString from 'lz-string'; import * as _ from 'lodash'; import isEqual from 'lodash/lang'; -import { Modifications } from 'coriolis-data/dist'; +import { Ships, Modifications } from 'coriolis-data/dist'; const zlib = require('zlib'); const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh']; @@ -660,7 +660,7 @@ export default class Ship { if (version != 2) { // Alter as required due to changes in the (build) code from one version to the next - this.upgradeInternals(this.id, internal, 1 + this.standard.length + this.hardpoints.length, priorities, enabled, modifications, blueprints, version); + this.upgradeInternals(internal, 1 + this.standard.length + this.hardpoints.length, priorities, enabled, modifications, blueprints, version); } return this.buildWith( @@ -1675,13 +1675,13 @@ export default class Ship { * @param {array} blueprints the existing blueprints arrray * @param {int} version the version of the information */ - upgradeInternals(shipId, internals, offset, priorities, enableds, modifications, blueprints, version) { + upgradeInternals(internals, offset, priorities, enableds, modifications, blueprints, version) { if (version == 1) { // Version 2 reflects the addition of military slots. this means that we need to juggle the internals and their // associated information around to make holes in the appropriate places for (let slotId = 0; slotId < this.internal.length; slotId++) { if (this.internal[slotId].eligible && this.internal[slotId].eligible.mrp) { - // Found an MRP - push all of the existing items down one to compensate for the fact that they didn't exist before now + // Found a restricted military slot - push all of the existing items down one to compensate for the fact that they didn't exist before now internals.push.apply(internals, [0].concat(internals.splice(slotId).slice(0, -1))); const offsetSlotId = offset + slotId; @@ -1693,6 +1693,8 @@ export default class Ship { if (blueprints) { blueprints.push.apply(blueprints, [null].concat(blueprints.splice(offsetSlotId).slice(0, -1))); } } } + // Ensure that all items are the correct length + internals.splice(Ships[this.id].slots.internal.length); } } } diff --git a/src/less/icons.less b/src/less/icons.less index 9af0c0e6..999352a9 100755 --- a/src/less/icons.less +++ b/src/less/icons.less @@ -34,8 +34,10 @@ width: 1.1em; height: 1em; stoke: @fg; + stroke-width: 20; fill: transparent; + &.sm { width: 0.8em; height: 0.75em;