Add 'Damage received' section

This commit is contained in:
Cmdr McDonald
2016-12-17 10:46:52 +00:00
parent 6ac69a6388
commit b8cff0c2fc
7 changed files with 393 additions and 40 deletions

View File

@@ -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

View File

@@ -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(<tr key={hardpoint}>
<td>{classRating} {slotName(translate, ship.hardpoints[hardpoint])}</td>
<td>{formats.round1(effectiveDps)}</td>
<td>{formats.round1(effectiveSDps)}</td>
<td>{formats.pct(effectiveness)}</td>
rows.push(<tr key={weapon.id}>
<td className='ri'>
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{weapon.classRating} {translate(weapon.name)}
</td>
<td className='ri'>{formats.round1(weapon.effectiveDps)}</td>
<td className='ri'>{formats.round1(weapon.effectiveSDps)}</td>
<td className='ri'>{formats.pct(weapon.effectiveness)}</td>
</tr>);
}
}
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 (
<span>
<h1>{translate('damage dealt against')}</h1>
<ShipSelector initial={this.state.against} currentMenu={this.props.currentMenu} onChange={this._onShipChange} />
<table style={{ width: '100%' }}>
<table className='summary' style={{ width: '100%' }}>
<thead>
<tr className='main'>
<td>{translate('weapon')}</td>
<td>{translate('effective dps')}</td>
<td>{translate('effective sdps')}</td>
<td>{translate('effectiveness')}</td>
<td className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</td>
<td className='sortable' onClick={sortOrder.bind(this, 'edps')}>{translate('effective dps')}</td>
<td className='sortable' onClick={sortOrder.bind(this, 'esdps')}>{translate('effective sdps')}</td>
<td className='sortable' onClick={sortOrder.bind(this, 'e')}>{translate('effectiveness')}</td>
</tr>
</thead>
<tbody>
{this._renderRows(translate, formats, ship, against)}
{this._renderRows(translate, formats)}
</tbody>
</table>
</span>

View File

@@ -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(<tr key={weapon.id}>
<td className='ri'>
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{weapon.classRating} {translate(weapon.name)}
</td>
<td>{formats.round1(weapon.effectiveDpsShields)}</td>
<td>{formats.round1(weapon.effectiveSDpsShields)}</td>
<td>{formats.pct(weapon.effectivenessShields)}</td>
<td>{formats.round1(weapon.effectiveDpsHull)}</td>
<td>{formats.round1(weapon.effectiveSDpsHull)}</td>
<td>{formats.pct(weapon.effectivenessHull)}</td>
</tr>);
}
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 (
<span>
<h1>{translate('damage received by')}</h1>
<table className='summary' style={{ width: '100%' }}>
<thead>
<tr className='main'>
<th rowSpan={2} className='sortable' onClick={sortOrder.bind(this, 'n')} >{translate('weapon')}</th>
<th colSpan={3} >{translate('against shields')}</th>
<th colSpan={3} >{translate('against hull')}</th>
</tr>
<tr>
<th className='sortable lft' onClick={sortOrder.bind(this, 'edpss')} onMouseOver={termtip.bind(null, 'dps')} onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}</th>
<th className='sortable' onClick={sortOrder.bind(this, 'esdpss')} onMouseOver={termtip.bind(null, 'sdps')} onMouseOut={tooltip.bind(null, null)}>{translate('SDPS')}</th>
<th className='sortable' onClick={sortOrder.bind(this, 'es')} >{translate('effectiveness')}</th>
<th className='sortable lft' onClick={sortOrder.bind(this, 'edpsh')} onMouseOver={termtip.bind(null, 'dps')} onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}</th>
<th className='sortable' onClick={sortOrder.bind(this, 'esdpsh')} onMouseOver={termtip.bind(null, 'sdps')} onMouseOut={tooltip.bind(null, null)}>{translate('SDPS')}</th>
<th className='sortable' onClick={sortOrder.bind(this, 'eh')} >{translate('effectiveness')}</th>
</tr>
</thead>
<tbody>
{this._renderRows(translate, formats)}
</tbody>
</table>
</span>
);
}
}

View File

@@ -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',

View File

@@ -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 {
<DamageDealt ship={ship} code={code} currentMenu={menu}/>
</div>
<div>
<DamageReceived ship={ship} code={code} currentMenu={menu}/>
</div>
</div>
);
}

View File

@@ -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);
}
}
}

View File

@@ -34,8 +34,10 @@
width: 1.1em;
height: 1em;
stoke: @fg;
stroke-width: 20;
fill: transparent;
&.sm {
width: 0.8em;
height: 0.75em;