diff --git a/package.json b/package.json
index 6f9d4a16..113b5fc9 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"less": "^2.5.3",
"less-loader": "^2.2.1",
"react-addons-test-utils": "^15.0.1",
+ "@types/react-measure": "^0.4.6",
"react-testutils-additions": "^15.1.0",
"rimraf": "^2.4.3",
"rollup": "0.36",
diff --git a/src/app/components/BattleCentre.jsx b/src/app/components/BattleCentre.jsx
index 21a10c29..d0f19138 100644
--- a/src/app/components/BattleCentre.jsx
+++ b/src/app/components/BattleCentre.jsx
@@ -9,7 +9,7 @@ import Cargo from './Cargo';
import Movement from './Movement';
import EngagementRange from './EngagementRange';
import ShipPicker from './ShipPicker';
-import Shields from './Shields';
+import Defence from './Defence';
/**
* Battle centre allows you to pit your current build against another ship,
@@ -113,28 +113,31 @@ export default class BattleCentre extends TranslatedComponent {
const { sys, eng, wep, cargo, fuel, boost, engagementRange, opponent } = this.state;
const { ship } = this.props;
- // Markers are used to propagate state changes
- const movementMarker = '' + ship.topSpeed + ':' + ship.pitch + ':' + ship.roll + ':' + ship.yaw;
- const shieldMarker = '' + ship.shield + ':' + ship.cells + ':' + ship.shieldExplRes + ':' + ship.shieldKinRes + ':' + ship.shieldThermRes;
+ // Markers are used to propagate state changes without requiring a deep comparison of the ship
+ const pipsMarker = '' + ship.canBoost();
+ const movementMarker = '' + ship.topSpeed + ':' + ship.pitch + ':' + ship.roll + ':' + ship.yaw + ':' + ship.canBoost();
+ const shieldMarker = '' + ship.shield + ':' + ship.shieldCells + ':' + ship.shieldExplRes + ':' + ship.shieldKinRes + ':' + ship.shieldThermRes + ':' + ship.armour;
return (
{translate('battle centre')}
-
-
+
{translate('ship management')}
+
-
+ { ship.cargoCapacity > 0 ? : null }
+ {translate('opponent')}
+
-
-
-
+
{translate('movement')}
+
+
{translate('defence')}
+
+
);
}
diff --git a/src/app/components/Defence.jsx b/src/app/components/Defence.jsx
new file mode 100644
index 00000000..03c3a3be
--- /dev/null
+++ b/src/app/components/Defence.jsx
@@ -0,0 +1,421 @@
+import React from 'react';
+import cn from 'classnames';
+import TranslatedComponent from './TranslatedComponent';
+import * as Calc from '../shipyard/Calculations';
+import { DamageAbsolute, DamageExplosive, DamageKinetic, DamageThermal } from './SvgIcons';
+import PieChart from './PieChart';
+import VerticalBarChart from './VerticalBarChart';
+
+/**
+ * Defence information
+ * Shield information consists of four panels:
+ * - textual information (time to lose shields etc.)
+ * - breakdown of shield sources (pie chart)
+ * - comparison of shield resistances (bar chart)
+ * - effective shield (bar chart)
+ */
+export default class Defence extends TranslatedComponent {
+ static propTypes = {
+ marker: React.PropTypes.string.isRequired,
+ ship: React.PropTypes.object.isRequired,
+ opponent: React.PropTypes.object.isRequired,
+ sys: React.PropTypes.number.isRequired
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ */
+ constructor(props) {
+ super(props);
+
+ const { shield, armour, damagetaken } = this._calcMetrics(props.ship, props.opponent, props.sys);
+ this.state = { shield, armour, damagetaken };
+ }
+
+ /**
+ * 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.sys != nextProps.sys) {
+ const { shield, armour, damagetaken } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys);
+ this.setState({ shield, armour, damagetaken });
+ return true;
+ }
+ }
+
+ /**
+ * Calculate shield metrics
+ * @param {Object} ship The ship
+ * @param {Object} opponent The opponent ship
+ * @param {int} sys The opponent ship
+ * @returns {Object} Shield metrics
+ */
+ _calcMetrics(ship, opponent, sys) {
+ const sysResistance = this._calcSysResistance(sys);
+
+ let shield = {};
+ const shieldGeneratorSlot = ship.findInternalByGroup('sg');
+ if (shieldGeneratorSlot && shieldGeneratorSlot.enabled && shieldGeneratorSlot.m) {
+ const shieldGenerator = shieldGeneratorSlot.m;
+
+ // Boosters
+ let boost = 1;
+ let boosterExplDmg = 1;
+ let boosterKinDmg = 1;
+ let boosterThermDmg = 1;
+ for (let slot of ship.hardpoints) {
+ if (slot.enabled && slot.m && slot.m.grp == 'sb') {
+ boost += slot.m.getShieldBoost();
+ boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance());
+ boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance());
+ boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance());
+ }
+ }
+
+ // Calculate diminishing returns for boosters
+ boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5);
+ // Remove base shield generator strength
+ boost -= 1;
+ // Apply diminishing returns
+ boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2;
+ boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2;
+ boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2;
+
+ const generatorStrength = Calc.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
+ const boostersStrength = generatorStrength * boost;
+ shield = {
+ generator: generatorStrength,
+ boosters: boostersStrength,
+ cells: ship.shieldCells,
+ total: generatorStrength + boostersStrength + ship.shieldCells
+ };
+
+ // Shield resistances have three components: the shield generator, the shield boosters and the SYS pips.
+ // We re-cast these as damage percentages
+ shield.absolute = {
+ generator: 1,
+ boosters: 1,
+ sys: 1 - sysResistance,
+ total: 1 - sysResistance
+ };
+
+ shield.explosive = {
+ generator: 1 - shieldGenerator.getExplosiveResistance(),
+ boosters: boosterExplDmg,
+ sys: (1 - sysResistance),
+ total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance)
+ };
+
+ shield.kinetic = {
+ generator: 1 - shieldGenerator.getKineticResistance(),
+ boosters: boosterKinDmg,
+ sys: (1 - sysResistance),
+ total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance)
+ };
+
+ shield.thermal = {
+ generator: 1 - shieldGenerator.getThermalResistance(),
+ boosters: boosterThermDmg,
+ sys: (1 - sysResistance),
+ total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance)
+ };
+ }
+
+ // Armour from bulkheads
+ const armourBulkheads = ship.baseArmour + (ship.baseArmour * ship.bulkheads.m.getHullBoost());
+ let armourReinforcement = 0
+
+ let modulearmour = 0;
+ let moduleprotection = 1;
+
+ let hullExplDmg = 1;
+ let hullKinDmg = 1;
+ let hullThermDmg = 1;
+
+ // Armour from HRPs and module armour from MRPs
+ for (let slot of ship.internal) {
+ if (slot.m && slot.m.grp == 'hr') {
+ armourReinforcement += slot.m.getHullReinforcement();
+ // Hull boost for HRPs is applied against the ship's base armour
+ armourReinforcement += ship.baseArmour * slot.m.getModValue('hullboost') / 10000;
+
+ hullExplDmg = hullExplDmg * (1 - slot.m.getExplosiveResistance());
+ hullKinDmg = hullKinDmg * (1 - slot.m.getKineticResistance());
+ hullThermDmg = hullThermDmg * (1 - slot.m.getThermalResistance());
+ }
+ if (slot.m && slot.m.grp == 'mrp') {
+ modulearmour += slot.m.getIntegrity();
+ moduleprotection = moduleprotection * (1 - slot.m.getProtection());
+ }
+ }
+ moduleprotection = 1 - moduleprotection;
+
+ // Apply diminishing returns
+ hullExplDmg = hullExplDmg > 0.7 ? hullExplDmg : 0.7 - (0.7 - hullExplDmg) / 2;
+ hullKinDmg = hullKinDmg > 0.7 ? hullKinDmg : 0.7 - (0.7 - hullKinDmg) / 2;
+ hullThermDmg = hullThermDmg > 0.7 ? hullThermDmg : 0.7 - (0.7 - hullThermDmg) / 2;
+
+ const armour = {
+ bulkheads: armourBulkheads,
+ reinforcement: armourReinforcement,
+ modulearmour: modulearmour,
+ moduleprotection: moduleprotection,
+ total: armourBulkheads + armourReinforcement
+ };
+
+
+ // Armour resistances have two components: bulkheads and HRPs
+ // We re-cast these as damage percentages
+ armour.absolute = {
+ bulkheads: 1,
+ reinforcement: 1,
+ total: 1
+ };
+
+ armour.explosive = {
+ bulkheads: 1 - ship.bulkheads.m.getExplosiveResistance(),
+ reinforcement: hullExplDmg,
+ total: (1 - ship.bulkheads.m.getExplosiveResistance()) * hullExplDmg
+ };
+
+ armour.kinetic = {
+ bulkheads: 1 - ship.bulkheads.m.getKineticResistance(),
+ reinforcement: hullKinDmg,
+ total: (1 - ship.bulkheads.m.getKineticResistance()) * hullKinDmg
+ };
+
+ armour.thermal = {
+ bulkheads: 1 - ship.bulkheads.m.getThermalResistance(),
+ reinforcement: hullThermDmg,
+ total: (1 - ship.bulkheads.m.getThermalResistance()) * hullThermDmg
+ };
+
+ // Use the SDPS for each weapon type of the opponent to work out how long the shields and armour will last
+ // const opponentSDps = Calc.sustainedDps(opponent, range);
+ const opponentSDps = {
+ absolute: 62.1,
+ explosive: 0,
+ kinetic: 7.4,
+ thermal: 7.4
+ };
+
+ // Modify according to resistances to see how much damage we actually take
+ //opponentSDps.absolute *= shield.absolute.total;
+ //opponentSDps.explosive *= shield.explosive.total;
+ //opponentSDps.kinetic *= shield.kinetic.total;
+ //opponentSDps.thermal *= shield.thermal.total;
+ opponentSDps.total = opponentSDps.absolute + opponentSDps.explosive + opponentSDps.kinetic + opponentSDps.thermal;
+
+ const damagetaken = {
+ absolutesdps: opponentSDps.absolute,
+ explosivesdps: opponentSDps.explosive,
+ kineticsdps: opponentSDps.kinetic,
+ thermalsdps: opponentSDps.thermal,
+ tts: (shield.total + ship.shieldCells) / opponentSDps.total,
+ };
+
+ return { shield, armour, damagetaken };
+ }
+
+ /**
+ * Calculate the resistance provided by SYS pips
+ * @param {integer} sys the value of the SYS pips
+ * @returns {integer} the resistance for the given pips
+ */
+ _calcSysResistance(sys) {
+ return Math.pow(sys,0.85) * 0.6 / Math.pow(4,0.85);
+ }
+
+ /**
+ * Render shields
+ * @return {React.Component} contents
+ */
+ render() {
+ const { ship, sys } = this.props;
+ const { language, tooltip, termtip } = this.context;
+ const { formats, translate, units } = language;
+ const { shield, armour, damagetaken } = this.state;
+
+ const shieldSourcesData = [];
+ const effectiveShieldData = [];
+ const damageTakenData = [];
+ const shieldTooltipDetails = [];
+ const shieldAbsoluteTooltipDetails = [];
+ const shieldExplosiveTooltipDetails = [];
+ const shieldKineticTooltipDetails = [];
+ const shieldThermalTooltipDetails = [];
+ let effectiveAbsoluteShield = 0;
+ let effectiveExplosiveShield = 0;
+ let effectiveKineticShield = 0;
+ let effectiveThermalShield = 0;
+ if (shield.total) {
+ if (Math.round(shield.generator) > 0) shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') });
+ if (Math.round(shield.boosters) > 0) shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') });
+ if (Math.round(shield.cells) > 0) shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') });
+
+ if (Math.round(shield.generator) > 0) shieldTooltipDetails.push({translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
);
+ if (Math.round(shield.boosters) > 0) shieldTooltipDetails.push({translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
);
+ if (Math.round(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)}
);
+ shieldAbsoluteTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}
);
+
+ shieldExplosiveTooltipDetails.push({translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}
);
+ shieldExplosiveTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}
);
+ shieldExplosiveTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}
);
+
+ shieldKineticTooltipDetails.push({translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}
);
+ shieldKineticTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}
);
+ shieldKineticTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}
);
+
+ shieldThermalTooltipDetails.push({translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}
);
+ shieldThermalTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}
);
+ shieldThermalTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}
);
+
+ effectiveAbsoluteShield = shield.total / shield.absolute.total;
+ effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') });
+ effectiveExplosiveShield = shield.total / shield.explosive.total;
+ effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') });
+ effectiveKineticShield = shield.total / shield.kinetic.total;
+ effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') });
+ effectiveThermalShield = shield.total / shield.thermal.total;
+ effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') });
+
+ damageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute') });
+ damageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive') });
+ damageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic') });
+ damageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal') });
+ }
+
+ const armourData = [];
+ if (Math.round(armour.bulkheads) > 0) armourData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') });
+ if (Math.round(armour.reinforcement) > 0) armourData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') });
+
+ const armourTooltipDetails = [];
+ 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)}
);
+ armourAbsoluteTooltipDetails.push({translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}
);
+
+ const armourExplosiveTooltipDetails = [];
+ armourExplosiveTooltipDetails.push({translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}
);
+ armourExplosiveTooltipDetails.push({translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}
);
+
+ const armourKineticTooltipDetails = [];
+ armourKineticTooltipDetails.push({translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}
);
+ armourKineticTooltipDetails.push({translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}
);
+
+ const armourThermalTooltipDetails = [];
+ armourThermalTooltipDetails.push({translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}
);
+ armourThermalTooltipDetails.push({translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}
);
+
+ return (
+
+ {shield.total ?
+
+
+
{shieldTooltipDetails}
)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ}
+
+
+
+ | {translate('damage type')} |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ | {translate('damage taken')} |
+
+ {shieldAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.absolute.total)}
+ |
+
+ {shieldExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.explosive.total)}
+ |
+
+ {shieldKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.kinetic.total)}
+ |
+
+ {shieldThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(shield.thermal.total)}
+ |
+
+
+ | {translate('effective shield')} |
+
+ {formats.int(effectiveAbsoluteShield)}{units.MJ}
+ |
+
+ {formats.int(effectiveExplosiveShield)}{units.MJ}
+ |
+
+ {formats.int(effectiveKineticShield)}{units.MJ}
+ |
+
+ {formats.int(effectiveThermalShield)}{units.MJ}
+ |
+
+
+ | {translate('shields will hold against opponent for')} {formats.time(damagetaken.tts)} |
+
+
+
+
+
+
{translate('shield sources')}
+
+
+
+
+
+
{translate('effective shield')}(MJ)
+
+
+
+
{translate('damage taken')}(%)
+
+
+
: null }
+
+
+
{armourTooltipDetails}
)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('armour')}: {formats.int(armour.total)}
+
+
+
+ | {translate('damage taken')} |
+
+ {armourAbsoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.absolute.total)}
+ |
+
+ {armourExplosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.explosive.total)}
+ |
+
+ {armourKineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.kinetic.total)}
+ |
+
+ {armourThermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(armour.thermal.total)}
+ |
+
+
+
+
+
+
{translate('armour sources')}
+
+
+ );
+ }
+}
diff --git a/src/app/components/EngagementRange.jsx b/src/app/components/EngagementRange.jsx
index 2f624bd0..ef851b01 100644
--- a/src/app/components/EngagementRange.jsx
+++ b/src/app/components/EngagementRange.jsx
@@ -27,10 +27,18 @@ export default class Range extends TranslatedComponent {
this.state = {
maxRange,
- rangeLevel: 1,
+ rangeLevel: 0.5,
};
}
+ /**
+ *
+ */
+ componentWillMount() {
+ // Pass initial state
+ this.props.onChange(this.state.maxRange * this.state.rangeLevel);
+ }
+
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx
new file mode 100644
index 00000000..c9199641
--- /dev/null
+++ b/src/app/components/PieChart.jsx
@@ -0,0 +1,78 @@
+import React, { Component } from 'react';
+import Measure from 'react-measure';
+import * as d3 from 'd3';
+
+const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ];
+const LABEL_COLOUR = '#FFFFFF';
+
+/**
+ * A pie chart
+ */
+export default class PieChart extends Component {
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ */
+ constructor(props, context) {
+ super(props);
+
+ this.pie = d3.pie().value((d) => d.value);
+ this.colors = CORIOLIS_COLOURS;
+ this.arc = d3.arc();
+ this.arc.innerRadius(0);
+
+ this.state = {
+ dimensions: {
+ width: 100,
+ height: 100
+ }
+ }
+ }
+
+
+ /**
+ * Generate a slice of the pie chart
+ */
+ sliceGenerator(d, i) {
+ const { width, height } = this.state.dimensions;
+ const { data } = this.props;
+
+ // Push the labels further out from the centre of the slice
+ let [labelX, labelY] = this.arc.centroid(d);
+ const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`;
+
+ // Put the keys in a line with equal spacing
+ const keyX = -width / 2 + (width / data.length) * (i + 0.5);
+ const keyTranslate = `translate(${keyX}, ${width * 0.45})`;
+
+ return (
+
+
+ {d.value}
+ {d.data.label}
+
+ );
+ }
+
+ render() {
+ const { width, height } = this.state.dimensions;
+ const pie = this.pie(this.props.data),
+ translate = `translate(${width / 2}, ${width * 0.4})`;
+
+ this.arc.outerRadius(width * 0.4);
+
+ return (
+ { this.setState({dimensions}) }}>
+
+
+
+
+ );
+ }
+}
diff --git a/src/app/components/Pips.jsx b/src/app/components/Pips.jsx
index 185ddf74..d5446594 100644
--- a/src/app/components/Pips.jsx
+++ b/src/app/components/Pips.jsx
@@ -15,6 +15,7 @@ import Module from '../shipyard/Module';
*/
export default class Pips extends TranslatedComponent {
static propTypes = {
+ marker: React.PropTypes.string.isRequired,
ship: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired
};
@@ -68,9 +69,9 @@ export default class Pips extends TranslatedComponent {
* @returns {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
- const { sysCap, engCap, wepCap, sysRate, engRate, wepRate } = this.state;
- const ship = nextProps.ship;
- const pd = ship.standard[4].m;
+ const { sysCap, engCap, wepCap, sysRate, engRate, wepRate, boost } = this.state;
+ const nextShip = nextProps.ship;
+ const pd = nextShip.standard[4].m;
const nextSysCap = pd.getSystemsCapacity();
const nextEngCap = pd.getEnginesCapacity();
@@ -78,19 +79,22 @@ export default class Pips extends TranslatedComponent {
const nextSysRate = pd.getSystemsRechargeRate();
const nextEngRate = pd.getEnginesRechargeRate();
const nextWepRate = pd.getWeaponsRechargeRate();
+ const nextBoost = nextShip.canBoost() ? boost : false;
if (nextSysCap != sysCap ||
nextEngCap != engCap ||
nextWepCap != wepCap ||
nextSysRate != sysRate ||
nextEngRate != engRate ||
- nextWepRate != wepRate) {
+ nextWepRate != wepRate ||
+ nextBoost != boost) {
this.setState({
sysCap: nextSysCap,
engCap: nextEngCap,
wepCap: nextWepCap,
sysRate: nextSysRate,
engRate: nextEngRate,
- wepRate: nextWepRate
+ wepRate: nextWepRate,
+ boost: nextBoost
});
}
@@ -104,8 +108,10 @@ export default class Pips extends TranslatedComponent {
_keyDown(e) {
switch (e.keyCode) {
case 9: // Tab == boost
- e.preventDefault();
- this._toggleBoost();
+ if (this.props.ship.canBoost()) {
+ e.preventDefault();
+ this._toggleBoost();
+ }
break;
case 37: // Left arrow == increase SYS
e.preventDefault();
@@ -364,21 +370,21 @@ export default class Pips extends TranslatedComponent {
{translate('RST')} |
{translate('WEP')} |
-
- | {translate('capacity')} ({units.MJ}) |
- {formats.f1(sysCap)} |
- {formats.f1(engCap)} |
- {formats.f1(wepCap)} |
-
-
- | {translate('recharge')} ({units.MW}) |
- {formats.f1(sysRate * (sys / 4))} |
- {formats.f1(engRate * (eng / 4))} |
- {formats.f1(wepRate * (wep / 4))} |
-
);
}
}
+//
+// | {translate('capacity')} ({units.MJ}) |
+// {formats.f1(sysCap)} |
+// {formats.f1(engCap)} |
+// {formats.f1(wepCap)} |
+//
+//
+// | {translate('recharge')} ({units.MW}) |
+// {formats.f1(sysRate * (sys / 4))} |
+// {formats.f1(engRate * (eng / 4))} |
+// {formats.f1(wepRate * (wep / 4))} |
+//
diff --git a/src/app/components/Shields.jsx b/src/app/components/Shields.jsx
deleted file mode 100644
index 445d8e72..00000000
--- a/src/app/components/Shields.jsx
+++ /dev/null
@@ -1,194 +0,0 @@
-import React from 'react';
-import cn from 'classnames';
-import TranslatedComponent from './TranslatedComponent';
-import * as Calc from '../shipyard/Calculations';
-import { DamageAbsolute, DamageExplosive, DamageKinetic, DamageThermal } from './SvgIcons';
-
-/**
- * Shields
- * Effective shield strength (including SCBs)
- * Time for opponent to down shields
- * - need sustained DPS for each type of damage (K/T/E/R)
- * - turn in to % of shields removed per second
- */
-export default class Shields extends TranslatedComponent {
- static propTypes = {
- marker: React.PropTypes.string.isRequired,
- ship: React.PropTypes.object.isRequired,
- opponent: React.PropTypes.object.isRequired,
- sys: React.PropTypes.number.isRequired
- };
-
- /**
- * Constructor
- * @param {Object} props React Component properties
- */
- constructor(props) {
- super(props);
-
- const { shield, absolute, explosive, kinetic, thermal } = this._calcMetrics(props.ship, props.opponent, props.sys);
- this.state = { shield, absolute, explosive, kinetic, thermal };
- }
-
- /**
- * 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.sys != nextProps.sys) {
- const { shield, absolute, explosive, kinetic, thermal } = this._calcMetrics(nextProps.ship, nextProps.opponent, nextProps.sys);
- this.setState({ shield, absolute, explosive, kinetic, thermal });
- return true;
- }
- }
-
- /**
- * Calculate shield metrics
- * @param {Object} ship The ship
- * @param {Object} opponent The opponent ship
- * @param {int} sys The opponent ship
- * @returns {Object} Shield metrics
- */
- _calcMetrics(ship, opponent, sys) {
- const sysResistance = this._calcSysResistance(sys);
-
- const shieldGenerator = ship.findShieldGenerator();
-
- // Boosters
- let boost = 1;
- let boosterExplDmg = 1;
- let boosterKinDmg = 1;
- let boosterThermDmg = 1;
- for (let slot of ship.hardpoints) {
- if (slot.enabled && slot.m && slot.m.grp == 'sb') {
- boost += slot.m.getShieldBoost();
- boosterExplDmg = boosterExplDmg * (1 - slot.m.getExplosiveResistance());
- boosterKinDmg = boosterKinDmg * (1 - slot.m.getKineticResistance());
- boosterThermDmg = boosterThermDmg * (1 - slot.m.getThermalResistance());
- }
- }
-
- // Calculate diminishing returns for boosters
- boost = Math.min(boost, (1 - Math.pow(Math.E, -0.7 * boost)) * 2.5);
- // Remove base shield generator strength
- boost -= 1;
- boosterExplDmg = boosterExplDmg > 0.7 ? boosterExplDmg : 0.7 - (0.7 - boosterExplDmg) / 2;
- boosterKinDmg = boosterKinDmg > 0.7 ? boosterKinDmg : 0.7 - (0.7 - boosterKinDmg) / 2;
- boosterThermDmg = boosterThermDmg > 0.7 ? boosterThermDmg : 0.7 - (0.7 - boosterThermDmg) / 2;
-
- const generatorStrength = Calc.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
- const boostersStrength = generatorStrength * boost;
- const shield = {
- generator: generatorStrength,
- boosters: boostersStrength,
- total: generatorStrength + boostersStrength
- };
-
- // Resistances have three components: the shield generator, the shield boosters and the SYS pips.
- // We re-cast these as damage percentages
- const absolute = {
- generator: 1,
- boosters: 1,
- sys: 1 - sysResistance,
- total: 1 - sysResistance
- };
-
- const explosive = {
- generator: 1 - shieldGenerator.getExplosiveResistance(),
- boosters: boosterExplDmg,
- sys: (1 - sysResistance),
- total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance)
- };
-
- const kinetic = {
- generator: 1 - shieldGenerator.getKineticResistance(),
- boosters: boosterKinDmg,
- sys: (1 - sysResistance),
- total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance)
- };
-
- const thermal = {
- generator: 1 - shieldGenerator.getThermalResistance(),
- boosters: boosterThermDmg,
- sys: (1 - sysResistance),
- total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance)
- };
-
- return { shield, absolute, explosive, kinetic, thermal };
- }
-
- /**
- * Calculate the resistance provided by SYS pips
- * @param {integer} sys the value of the SYS pips
- * @returns {integer} the resistance for the given pips
- */
- _calcSysResistance(sys) {
- return Math.pow(sys,0.85) * 0.6 / Math.pow(4,0.85);
- }
-
- /**
- * Render shields
- * @return {React.Component} contents
- */
- render() {
- const { ship, sys } = this.props;
- const { language, tooltip, termtip } = this.context;
- const { formats, translate, units } = language;
- const { shield, absolute, explosive, kinetic, thermal } = this.state;
-
- const shieldTooltipDetails = [];
- shieldTooltipDetails.push({translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}
);
- shieldTooltipDetails.push({translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}
);
-
- const absoluteTooltipDetails = [];
- absoluteTooltipDetails.push({translate('generator') + ' ' + formats.pct1(absolute.generator)}
);
- absoluteTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(absolute.boosters)}
);
- absoluteTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(absolute.sys)}
);
-
- const explosiveTooltipDetails = [];
- explosiveTooltipDetails.push({translate('generator') + ' ' + formats.pct1(explosive.generator)}
);
- explosiveTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(explosive.boosters)}
);
- explosiveTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(explosive.sys)}
);
-
- const kineticTooltipDetails = [];
- kineticTooltipDetails.push({translate('generator') + ' ' + formats.pct1(kinetic.generator)}
);
- kineticTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(kinetic.boosters)}
);
- kineticTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(kinetic.sys)}
);
-
- const thermalTooltipDetails = [];
- thermalTooltipDetails.push({translate('generator') + ' ' + formats.pct1(thermal.generator)}
);
- thermalTooltipDetails.push({translate('boosters') + ' ' + formats.pct1(thermal.boosters)}
);
- thermalTooltipDetails.push({translate('power distributor') + ' ' + formats.pct1(thermal.sys)}
);
-
- return (
-
-
-
-
- | {shieldTooltipDetails})} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('shields')}: {formats.int(shield.total)}{units.MJ} |
-
-
- | {translate('damage from')} |
-
-
- {absoluteTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(absolute.total)}
- |
-
-
- {explosiveTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(explosive.total)}
- |
-
-
- {kineticTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(kinetic.total)}
- |
-
-
- {thermalTooltipDetails})} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(thermal.total)}
- |
-
-
-
- );
- }
-}
diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx
new file mode 100644
index 00000000..1eab7614
--- /dev/null
+++ b/src/app/components/VerticalBarChart.jsx
@@ -0,0 +1,122 @@
+import React, { Component } from 'react';
+import Measure from 'react-measure';
+import * as d3 from 'd3';
+
+const CORIOLIS_COLOURS = [ '#FF8C0D', '#1FB0FF', '#519032', '#D5420D' ];
+const LABEL_COLOUR = '#FFFFFF';
+
+var margin = {top: 10, right: 0, bottom: 0, left: 50};
+
+const ASPECT = 1;
+
+const merge = function(one, two) {
+ return Object.assign({}, one, two);
+};
+
+/**
+ * A vertical bar chart
+ */
+export default class VerticalBarChart extends Component {
+
+ static propTypes = {
+ data : React.PropTypes.array.isRequired,
+ ylabel : React.PropTypes.string
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ */
+ constructor(props, context) {
+ super(props);
+
+ this.state = {
+ dimensions: {
+ width: 300,
+ height: 300
+ }
+ }
+ }
+
+ _renderGraph(props){
+ let { width, height } = this.state.dimensions;
+
+ width = width - margin.left - margin.right,
+ height = width * ASPECT - margin.top - margin.bottom;
+
+ // X axis is a band scale with values being 'label'
+ this.x = d3.scaleBand();
+ this.x.domain(this.props.data.map(d => d.label)).padding(0.2);
+ this.xAxis = d3.axisBottom(this.x).tickValues(this.props.data.map(d => d.label));
+ this.x.range([0, width]);
+
+ // Y axis is a numeric scale with values being 'value'
+ this.y = d3.scaleLinear();
+ this.y.domain([0, d3.max(this.props.data, d => d.value)]);
+ this.yAxis = d3.axisLeft(this.y);
+ this.y.range([height, 0]);
+
+ let svg = d3.select(this.svg).select('g');
+
+ svg.selectAll('rect').remove();
+ svg.selectAll('text').remove();
+
+ svg.select('.x.axis').remove();
+ svg.select('.y.axis').remove();
+
+ svg.append('g')
+ .attr('class', 'x axis')
+ .attr('transform', `translate(0, ${height})`)
+ .call(this.xAxis);
+
+ svg.append('g')
+ .attr('class', 'y axis')
+ .call(this.yAxis)
+ .attr('fill', CORIOLIS_COLOURS[0])
+
+ svg.selectAll('rect.bar')
+ .data(props.data)
+ .enter().append('rect')
+ .attr('class', 'bar')
+ .attr('x', d => this.x(d.label))
+ .attr('width', this.x.bandwidth())
+ .attr('y', d => this.y(d.value))
+ .attr('height', d => height - this.y(d.value))
+ .attr('fill', CORIOLIS_COLOURS[0]);
+
+ svg.selectAll('text.bar')
+ .data(props.data)
+ .enter().append('text')
+ .attr('class', 'bar')
+ .attr('text-anchor', 'middle')
+ .attr('x', 100)
+ .attr('y', 100)
+ .attr('stroke', '#ffffff')
+ .attr('stroke-width', '1px')
+ .attr('x', d => this.x(d.label) + this.x.bandwidth() / 2)
+ .attr('y', d => this.y(d.value) + 15)
+ .text(d => d.value);
+ }
+
+
+
+ render() {
+ const { width } = this.state.dimensions;
+
+ const translate = `translate(${margin.left}, ${margin.top})`;
+
+ this._renderGraph(this.props);
+
+ return (
+ { this.setState({dimensions}) }}>
+
+ { this.x ?
+ : null }
+
+
+ );
+ }
+}
diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js
index 10da4582..785c8d27 100755
--- a/src/app/shipyard/Ship.js
+++ b/src/app/shipyard/Ship.js
@@ -1348,13 +1348,13 @@ export default class Ship {
let moduleprotection = 1;
let hullExplRes = 1 - bulkhead.getExplosiveResistance();
const hullExplResDRStart = hullExplRes * 0.7;
- const hullExplResDREnd = hullExplRes * 0; // Currently don't know where this is
+ const hullExplResDREnd = hullExplRes * 0;
let hullKinRes = 1 - bulkhead.getKineticResistance();
const hullKinResDRStart = hullKinRes * 0.7;
- const hullKinResDREnd = hullKinRes * 0; // Currently don't know where this is
+ const hullKinResDREnd = hullKinRes * 0;
let hullThermRes = 1 - bulkhead.getThermalResistance();
const hullThermResDRStart = hullThermRes * 0.7;
- const hullThermResDREnd = hullThermRes * 0; // Currently don't know where this is
+ const hullThermResDREnd = hullThermRes * 0;
// Armour from HRPs and module armour from MRPs
for (let slot of this.internal) {
diff --git a/src/less/app.less b/src/less/app.less
index eb14b618..3dc03d2d 100755
--- a/src/less/app.less
+++ b/src/less/app.less
@@ -22,6 +22,7 @@
@import 'pips';
@import 'movement';
@import 'shippicker';
+@import 'defence';
html, body {
height: 100%;
diff --git a/src/less/defence.less b/src/less/defence.less
new file mode 100755
index 00000000..2c5c730e
--- /dev/null
+++ b/src/less/defence.less
@@ -0,0 +1,14 @@
+#defence {
+ table {
+ background-color: @bgBlack;
+ color: @primary;
+ margin: 0 auto;
+ }
+
+ .icon {
+ stroke: @primary;
+ stroke-width: 20;
+ fill: transparent;
+ }
+}
+
diff --git a/src/less/outfit.less b/src/less/outfit.less
index 7b792542..b97c354c 100755
--- a/src/less/outfit.less
+++ b/src/less/outfit.less
@@ -206,6 +206,14 @@
});
}
+ &.full {
+ width: 100%;
+
+ .smallTablet({
+ width: 100% !important;
+ });
+ }
+
.smallScreen({
.axis.x {
g.tick:nth-child(2n + 1) text {
diff --git a/src/less/pips.less b/src/less/pips.less
index 10220914..1db973ae 100755
--- a/src/less/pips.less
+++ b/src/less/pips.less
@@ -1,8 +1,10 @@
// The pips table - keep the background black
#pips {
+
table {
background-color: @bgBlack;
color: @primary;
+ margin: 0 auto;
}
// A clickable entity in the pips table