From 0729fc29fa5164a4e621ad46409e9a72bbee4f05 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Mon, 13 Mar 2017 17:07:39 +0000 Subject: [PATCH] Add vertical bar chart and use it in battle centre --- package.json | 1 + src/app/components/BattleCentre.jsx | 27 +- src/app/components/Defence.jsx | 421 ++++++++++++++++++++++++ src/app/components/EngagementRange.jsx | 10 +- src/app/components/PieChart.jsx | 78 +++++ src/app/components/Pips.jsx | 44 +-- src/app/components/Shields.jsx | 194 ----------- src/app/components/VerticalBarChart.jsx | 122 +++++++ src/app/shipyard/Ship.js | 6 +- src/less/app.less | 1 + src/less/defence.less | 14 + src/less/outfit.less | 8 + src/less/pips.less | 2 + 13 files changed, 699 insertions(+), 229 deletions(-) create mode 100644 src/app/components/Defence.jsx create mode 100644 src/app/components/PieChart.jsx delete mode 100644 src/app/components/Shields.jsx create mode 100644 src/app/components/VerticalBarChart.jsx create mode 100755 src/less/defence.less 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}) }}> +
+ + + {pie.map((d, i) => this.sliceGenerator(d, i))} + + +
+
+ ); + } +} 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 ? + this.svg = ref} width={width} height={width * ASPECT} transform={translate}> + + : 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