Merge branch 'feature/battlecentre' into develop

This commit is contained in:
Cmdr McDonald
2017-03-23 08:20:14 +00:00
59 changed files with 3941 additions and 553 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ nginx.pid
.idea .idea
/bin /bin
env env
*.swp

View File

@@ -8,6 +8,11 @@
* Show integrity value for relevant modules * Show integrity value for relevant modules
* Reset old modification values when a new roll is applied * Reset old modification values when a new roll is applied
* Fix issue with miner role where refinery would not be present in ships with class 5 slots but no class 4 * Fix issue with miner role where refinery would not be present in ships with class 5 slots but no class 4
* Ensure that boost value is set correctly when modifications to power distributor enable/disable boost
* Ensure that hull reinforcement modifications take the inherent resistance in to account when calculating modification percentages
* Add tooltip for blueprints providing details of the features they alter
* Use opponent's saved pips if available
* Ignore rounds per shot for EPS and HPS calculations; it's already factored in to the numbers
#2.2.19 #2.2.19
* Power management panel now displays modules in descending order of power usage by default * Power management panel now displays modules in descending order of power usage by default

View File

@@ -320,7 +320,6 @@
"shieldExplRes": 0.5, "shieldExplRes": 0.5,
"shieldKinRes": 0.4, "shieldKinRes": 0.4,
"shieldThermRes": -0.2, "shieldThermRes": -0.2,
"timeToDrain": 7.04,
"crew": 3 "crew": 3
} }
} }

View File

@@ -36,7 +36,7 @@
"Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=." "Test": "A4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04---0303326b.Iw18ZVA=.Aw18ZVA=."
}, },
"diamondback_explorer": { "diamondback_explorer": {
"Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f.AwRj4zTI.AwiMIypI." "Explorer": "A0p0tdFfldddsdf5---0202--320p432i2f-.AwRj4zTYg===.AwiMIyoo."
}, },
"vulture": { "vulture": {
"Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA." "Bounty Hunter": "A3patcFalddksff31e1e0404-0l4a-5d27662j.AwRj4z2I.MwBhBYy6oJmAjLIA."

8
d3.min.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -76,7 +76,9 @@
"json-loader": "^0.5.3", "json-loader": "^0.5.3",
"less": "^2.5.3", "less": "^2.5.3",
"less-loader": "^2.2.1", "less-loader": "^2.2.1",
"react-addons-perf": "^15.4.2",
"react-addons-test-utils": "^15.0.1", "react-addons-test-utils": "^15.0.1",
"react-measure": "^1.4.6",
"react-testutils-additions": "^15.1.0", "react-testutils-additions": "^15.1.0",
"rimraf": "^2.4.3", "rimraf": "^2.4.3",
"rollup": "0.36", "rollup": "0.36",
@@ -88,16 +90,17 @@
}, },
"dependencies": { "dependencies": {
"babel-polyfill": "*", "babel-polyfill": "*",
"classnames": "^2.2.0",
"browserify-zlib": "ipfs/browserify-zlib", "browserify-zlib": "ipfs/browserify-zlib",
"classnames": "^2.2.0",
"coriolis-data": "EDCD/coriolis-data", "coriolis-data": "EDCD/coriolis-data",
"d3": "4.6.0", "d3": "4.6.0",
"fbemitter": "^2.0.0", "fbemitter": "^2.0.0",
"lodash": "^4.15.0", "lodash": "^4.15.0",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
"react": "^15.0.1", "react": "^15.0.1",
"react-dom": "^15.0.1", "react-dom": "^15.0.1",
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
"recharts": "^0.21.2",
"superagent": "^1.4.0" "superagent": "^1.4.0"
} }
} }

View File

@@ -241,14 +241,19 @@ export default class Coriolis extends React.Component {
/** /**
* Show the term tip * Show the term tip
* @param {string} term Term or Phrase * @param {string} term Term or Phrase
* @param {Object} opts Options - dontCap, orientation (n,e,s,w) * @param {Object} opts Options - dontCap, orientation (n,e,s,w) (can also be the event if no options supplied)
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
* @param {SyntheticEvent} e2 Alternative location for synthetic event from charts (where 'Event' is actually a chart index)
*/ */
_termtip(term, opts, event) { _termtip(term, opts, event, e2) {
if (opts && opts.nativeEvent) { // Opts is a SyntheticEvent if (opts && opts.nativeEvent) { // Opts is the SyntheticEvent
event = opts; event = opts;
opts = { cap: true }; opts = { cap: true };
} }
if (e2 instanceof Object && e2.nativeEvent) { // E2 is the SyntheticEvent
event = e2;
}
this._tooltip( this._tooltip(
<div className={'cen' + (opts.cap ? ' cap' : '')}>{this.state.language.translate(term)}</div>, <div className={'cen' + (opts.cap ? ' cap' : '')}>{this.state.language.translate(term)}</div>,
event.currentTarget.getBoundingClientRect(), event.currentTarget.getBoundingClientRect(),

View File

@@ -44,7 +44,7 @@ export default class BarChart extends TranslatedComponent {
unit: '' unit: ''
}; };
static PropTypes = { static propTypes = {
colors: React.PropTypes.array, colors: React.PropTypes.array,
data: React.PropTypes.array.isRequired, data: React.PropTypes.array.isRequired,
desc: React.PropTypes.bool, desc: React.PropTypes.bool,

View File

@@ -0,0 +1,90 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import ShipSelector from './ShipSelector';
import { nameComparator } from '../utils/SlotFunctions';
import { Pip } from './SvgIcons';
import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
/**
* Boost displays a boost button that toggles bosot
* Requires an onChange() function of the form onChange(boost) which is triggered whenever the boost changes.
*/
export default class Boost extends TranslatedComponent {
static propTypes = {
marker: React.PropTypes.string.isRequired,
ship: React.PropTypes.object.isRequired,
boost: React.PropTypes.bool.isRequired,
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
const { ship, boost } = props;
this._keyDown = this._keyDown.bind(this);
this._toggleBoost = this._toggleBoost.bind(this);
}
/**
* Add listeners after mounting
*/
componentDidMount() {
document.addEventListener('keydown', this._keyDown);
}
/**
* Remove listeners before unmounting
*/
componentWillUnmount() {
document.removeEventListener('keydown', this._keyDown);
}
/**
* Handle Key Down
* @param {Event} e Keyboard Event
*/
_keyDown(e) {
if (e.ctrlKey || e.metaKey) { // CTRL/CMD
switch (e.keyCode) {
case 66: // b == boost
if (this.props.ship.canBoost()) {
e.preventDefault();
this._toggleBoost();
}
break;
}
}
}
/**
* Toggle the boost feature
*/
_toggleBoost() {
this.props.onChange(!this.props.boost);
}
/**
* Render boost
* @return {React.Component} contents
*/
render() {
const { formats, translate, units } = this.context.language;
const { ship, boost } = this.props;
// TODO disable if ship cannot boost
return (
<span id='boost'>
<button id='boost' className={boost ? 'selected' : null} onClick={this._toggleBoost}>{translate('boost')}</button>
</span>
);
}
}

View File

@@ -0,0 +1,75 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import Slider from '../components/Slider';
/**
* Cargo slider
* Requires an onChange() function of the form onChange(cargo), providing the cargo in tonnes, which is triggered on cargo level change
*/
export default class Cargo extends TranslatedComponent {
static propTypes = {
cargo: React.PropTypes.number.isRequired,
cargoCapacity: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this._cargoChange = this._cargoChange.bind(this);
}
/**
* Update cargo level
* @param {number} cargoLevel percentage level from 0 to 1
*/
_cargoChange(cargoLevel) {
const { cargo, cargoCapacity } = this.props;
if (cargoCapacity > 0) {
// We round the cargo to whole number of tonnes
const newCargo = Math.round(cargoLevel * cargoCapacity);
if (newCargo != cargo) {
this.props.onChange(newCargo);
}
}
}
/**
* Render cargo slider
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { cargo, cargoCapacity } = this.props;
return (
<span>
<h3>{translate('cargo carried')}: {formats.int(cargo)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._cargoChange}
axisUnit={translate('T')}
percent={cargo / cargoCapacity}
max={cargoCapacity}
scale={sizeRatio}
onResize={onWindowResize}
/>
</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -12,7 +12,7 @@ import TranslatedComponent from './TranslatedComponent';
*/ */
export default class CostSection extends TranslatedComponent { export default class CostSection extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
code: React.PropTypes.string.isRequired, code: React.PropTypes.string.isRequired,
buildName: React.PropTypes.string buildName: React.PropTypes.string

View File

@@ -50,9 +50,8 @@ export function weaponComparator(translate, propComparator, desc) {
* Damage against a selected ship * Damage against a selected ship
*/ */
export default class DamageDealt extends TranslatedComponent { export default class DamageDealt extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
chartWidth: React.PropTypes.number.isRequired,
code: React.PropTypes.string.isRequired code: React.PropTypes.string.isRequired
}; };
@@ -556,7 +555,6 @@ export default class DamageDealt extends TranslatedComponent {
<div className='group half'> <div className='group half'>
<h1>{translate('sustained dps against standard shields')}</h1> <h1>{translate('sustained dps against standard shields')}</h1>
<LineChart <LineChart
width={this.props.chartWidth}
xMax={maxRange} xMax={maxRange}
yMax={this.state.maxDps} yMax={this.state.maxDps}
xLabel={translate('range')} xLabel={translate('range')}
@@ -572,7 +570,6 @@ export default class DamageDealt extends TranslatedComponent {
<div className='group half'> <div className='group half'>
<h1>{translate('sustained dps against standard armour')}</h1> <h1>{translate('sustained dps against standard armour')}</h1>
<LineChart <LineChart
width={this.props.chartWidth}
xMax={maxRange} xMax={maxRange}
yMax={this.state.maxDps} yMax={this.state.maxDps}
xLabel={translate('range')} xLabel={translate('range')}

View File

@@ -45,7 +45,7 @@ export function weaponComparator(translate, propComparator, desc) {
* Damage received by a selected ship * Damage received by a selected ship
*/ */
export default class DamageReceived extends TranslatedComponent { export default class DamageReceived extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
code: React.PropTypes.string.isRequired code: React.PropTypes.string.isRequired
}; };

View File

@@ -0,0 +1,200 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations';
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,
engagementrange: React.PropTypes.number.isRequired,
sys: React.PropTypes.number.isRequired,
opponentWep: React.PropTypes.number.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.opponentWep, props.engagementrange);
this.state = { shield, armour, shielddamage, armourdamage };
}
/**
* 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, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.opponentWep, nextProps.engagementrange);
this.setState({ shield, armour, shielddamage, armourdamage });
}
return true;
}
/**
* Render defence
* @return {React.Component} contents
*/
render() {
const { ship, sys, opponentWep } = this.props;
const { language, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { shield, armour, shielddamage, armourdamage } = this.state;
const pd = ship.standard[4].m;
const shieldSourcesData = [];
const effectiveShieldData = [];
const shieldDamageTakenData = [];
const shieldTooltipDetails = [];
const shieldAbsoluteTooltipDetails = [];
const shieldExplosiveTooltipDetails = [];
const shieldKineticTooltipDetails = [];
const shieldThermalTooltipDetails = [];
let maxEffectiveShield = 0;
if (shield.total) {
shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') });
shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') });
shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') });
if (shield.generator > 0) shieldTooltipDetails.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
if (shield.boosters > 0) shieldTooltipDetails.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
if (shield.cells > 0) shieldTooltipDetails.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
shieldAbsoluteTooltipDetails.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}</div>);
shieldAbsoluteTooltipDetails.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}</div>);
shieldAbsoluteTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}</div>);
shieldExplosiveTooltipDetails.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}</div>);
shieldExplosiveTooltipDetails.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}</div>);
shieldExplosiveTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}</div>);
shieldKineticTooltipDetails.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}</div>);
shieldKineticTooltipDetails.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}</div>);
shieldKineticTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}</div>);
shieldThermalTooltipDetails.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}</div>);
shieldThermalTooltipDetails.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}</div>);
shieldThermalTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}</div>);
const effectiveAbsoluteShield = shield.total / shield.absolute.total;
effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute') });
const effectiveExplosiveShield = shield.total / shield.explosive.total;
effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive') });
const effectiveKineticShield = shield.total / shield.kinetic.total;
effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic') });
const effectiveThermalShield = shield.total / shield.thermal.total;
effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal') });
shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldAbsoluteTooltipDetails });
shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldExplosiveTooltipDetails });
shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldKineticTooltipDetails });
shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldThermalTooltipDetails });
maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max);
}
const armourSourcesData = [];
armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') });
armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') });
const armourTooltipDetails = [];
if (armour.bulkheads > 0) armourTooltipDetails.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
if (armour.reinforcement > 0) armourTooltipDetails.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
const armourAbsoluteTooltipDetails = [];
armourAbsoluteTooltipDetails.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}</div>);
armourAbsoluteTooltipDetails.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}</div>);
const armourExplosiveTooltipDetails = [];
armourExplosiveTooltipDetails.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}</div>);
armourExplosiveTooltipDetails.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}</div>);
const armourKineticTooltipDetails = [];
armourKineticTooltipDetails.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}</div>);
armourKineticTooltipDetails.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}</div>);
const armourThermalTooltipDetails = [];
armourThermalTooltipDetails.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}</div>);
armourThermalTooltipDetails.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}</div>);
const effectiveArmourData = [];
const effectiveAbsoluteArmour = armour.total / armour.absolute.total;
effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute') });
const effectiveExplosiveArmour = armour.total / armour.explosive.total;
effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive') });
const effectiveKineticArmour = armour.total / armour.kinetic.total;
effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic') });
const effectiveThermalArmour = armour.total / armour.thermal.total;
effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal') });
const armourDamageTakenData = [];
armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourAbsoluteTooltipDetails });
armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourExplosiveTooltipDetails });
armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourKineticTooltipDetails });
armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourThermalTooltipDetails });
return (
<span id='defence'>
{shield.total ? <span>
<div className='group quarter'>
<h2>{translate('shield metrics')}</h2>
<br/>
<h2 onMouseOver={termtip.bind(null, <div>{shieldTooltipDetails}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shield.total)}{units.MJ}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}<br/>{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECOVER'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}<br/>{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECHARGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}<br/>{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}</h2>
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
<PieChart data={shieldSourcesData} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2>
<VerticalBarChart data={shieldDamageTakenData} yMax={140} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_SHIELD'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective shield')}(MJ)</h2>
<VerticalBarChart data={effectiveShieldData} yMax={maxEffectiveShield}/>
</div>
</span> : null }
<div className='group quarter'>
<h2>{translate('armour metrics')}</h2>
<h2 onMouseOver={termtip.bind(null, <div>{armourTooltipDetails}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}<br/>{formats.int(armour.total)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}<br/>{armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('raw module armour')}<br/>{formats.int(armour.modulearmour)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_EXTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}<br/>{formats.pct1(armour.moduleprotection / 2)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_INTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}<br/>{formats.pct1(armour.moduleprotection)}</h2>
<br/>
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour sources')}</h2>
<PieChart data={armourSourcesData} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_DAMAGE_TAKEN'))} onMouseOut={tooltip.bind(null, null)}>{translate('damage taken')}(%)</h2>
<VerticalBarChart data={armourDamageTakenData} yMax={100} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_EFFECTIVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('effective armour')}</h2>
<VerticalBarChart data={effectiveArmourData} />
</div>
</span>);
}
}

View File

@@ -7,7 +7,7 @@ import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
* Defence summary * Defence summary
*/ */
export default class DefenceSummary extends TranslatedComponent { export default class DefenceSummary extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired ship: React.PropTypes.object.isRequired
}; };

View File

@@ -0,0 +1,101 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import Slider from '../components/Slider';
/**
* Engagement range slider
* Requires an onChange() function of the form onChange(range), providing the range in metres, which is triggered on range change
*/
export default class EngagementRange extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
engagementRange: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
const { ship } = props;
const maxRange = this._calcMaxRange(ship);
this.state = {
maxRange
};
}
/**
* Calculate the maximum range of a ship's weapons
* @param {Object} ship The ship
* @returns {int} The maximum range, in metres
*/
_calcMaxRange(ship) {
let maxRange = 1000;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const thisRange = ship.hardpoints[i].m.getRange();
if (thisRange > maxRange) {
maxRange = thisRange;
}
}
}
return maxRange;
}
/**
* Update range
* @param {number} rangeLevel percentage level from 0 to 1
*/
_rangeChange(rangeLevel) {
const { maxRange } = this.state;
// We round the range to an integer value
const range = Math.round(rangeLevel * maxRange);
if (range !== this.props.engagementRange) {
this.props.onChange(range);
}
}
/**
* Render range slider
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { engagementRange } = this.props;
const { maxRange } = this.state;
return (
<span>
<h3>{translate('engagement range')}: {formats.int(engagementRange)}{translate('m')}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._rangeChange.bind(this)}
axisUnit={translate('m')}
percent={engagementRange / maxRange}
max={maxRange}
scale={sizeRatio}
onResize={onWindowResize}
/>
</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -13,10 +13,13 @@ import * as Calc from '../shipyard/Calculations';
* Engine profile for a given ship * Engine profile for a given ship
*/ */
export default class EngineProfile extends TranslatedComponent { export default class EngineProfile extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
chartWidth: React.PropTypes.number.isRequired, cargo: React.PropTypes.number.isRequired,
code: React.PropTypes.string.isRequired fuel: React.PropTypes.number.isRequired,
eng: React.PropTypes.number.isRequired,
boost: React.PropTypes.bool.isRequired,
marker: React.PropTypes.string.isRequired
}; };
/** /**
@@ -30,8 +33,7 @@ export default class EngineProfile extends TranslatedComponent {
const ship = this.props.ship; const ship = this.props.ship;
this.state = { this.state = {
cargo: ship.cargoCapacity, calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost)
calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, ship)
}; };
} }
@@ -42,36 +44,23 @@ export default class EngineProfile extends TranslatedComponent {
* @return {boolean} Returns true if the component should be rerendered * @return {boolean} Returns true if the component should be rerendered
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.code != this.props.code) { if (nextProps.marker != this.props.marker) {
this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxSpeedFunc: this._calcMaxSpeed.bind(this, nextProps.ship) }); this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) });
} }
return true; return true;
} }
/** /**
* Calculate the maximum speed for this ship across its applicable mass * Calculate the top speed for this ship given thrusters, mass and pips to ENG
* @param {Object} ship The ship * @param {Object} ship The ship
* @param {Object} eng The number of pips to ENG
* @param {Object} boost If boost is enabled
* @param {Object} mass The mass at which to calculate the top speed * @param {Object} mass The mass at which to calculate the top speed
* @return {number} The maximum speed * @return {number} The maximum speed
*/ */
_calcMaxSpeed(ship, mass) { calcMaxSpeed(ship, eng, boost, mass) {
// Obtain the thrusters for this ship
const thrusters = ship.standard[1].m;
// Obtain the top speed // Obtain the top speed
return Calc.speed(mass, ship.speed, thrusters, ship.engpip)[4]; return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost);
}
/**
* Update cargo level
* @param {number} cargoLevel Cargo level 0 - 1
*/
_cargoChange(cargoLevel) {
let ship = this.props.ship;
let cargo = Math.round(ship.cargoCapacity * cargoLevel);
this.setState({
cargo
});
} }
/** /**
@@ -81,64 +70,37 @@ export default class EngineProfile extends TranslatedComponent {
render() { render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language; const { formats, translate, units } = language;
const { ship } = this.props; const { ship, cargo, eng, fuel, boost } = this.props;
const { cargo } = this.state;
// Calculate bounds for our line chart // Calculate bounds for our line chart
const thrusters = ship.standard[1].m; const thrusters = ship.standard[1].m;
const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const minMass = ship.calcLowestPossibleMass({ th: thrusters });
const maxMass = thrusters.getMaxMass(); const maxMass = thrusters.getMaxMass();
let mass = ship.unladenMass + ship.fuelCapacity + cargo; const mass = ship.unladenMass + fuel + cargo;
const minSpeed = Calc.speed(maxMass, ship.speed, thrusters, ship.engpip)[4]; const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false);
const maxSpeed = Calc.speed(minMass, ship.speed, thrusters, ship.engpip)[4]; const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true);
// Add a mark at our current mass // Add a mark at our current mass
const mark = Math.min(mass, maxMass); const mark = Math.min(mass, maxMass);
const cargoPercent = cargo / ship.cargoCapacity; const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`;
const code = ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString(); // This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother
// This graph has a precipitous fall-off so we use lots of points to make it look a little smoother
return ( return (
<span> <LineChart
<h1>{translate('engine profile')}</h1> xMin={minMass}
<LineChart xMax={maxMass}
width={this.props.chartWidth} yMin={minSpeed}
xMin={minMass} yMax={maxSpeed}
xMax={maxMass} xMark={mark}
yMin={minSpeed} xLabel={translate('mass')}
yMax={maxSpeed} xUnit={translate('T')}
xMark={mark} yLabel={translate('maximum speed')}
xLabel={translate('mass')} yUnit={translate('m/s')}
xUnit={translate('T')} func={this.state.calcMaxSpeedFunc}
yLabel={translate('maximum speed')} points={1000}
yUnit={translate('m/s')} code={code}
func={this.state.calcMaxSpeedFunc} aspect={0.7}
points={1000} />
code={code}
/>
{ship.cargoCapacity ?
<span>
<h3>{translate('cargo carried')}: {formats.int(cargo)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._cargoChange.bind(this)}
axisUnit={translate('T')}
percent={cargoPercent}
max={ship.cargoCapacity}
scale={sizeRatio}
onResize={onWindowResize}
/>
</td>
</tr>
</tbody>
</table>
</span> : '' }
</span>
); );
} }
} }

View File

@@ -13,10 +13,11 @@ import * as Calc from '../shipyard/Calculations';
* FSD profile for a given ship * FSD profile for a given ship
*/ */
export default class FSDProfile extends TranslatedComponent { export default class FSDProfile extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
chartWidth: React.PropTypes.number.isRequired, cargo: React.PropTypes.number.isRequired,
code: React.PropTypes.string.isRequired fuel: React.PropTypes.number.isRequired,
marker: React.PropTypes.string.isRequired
}; };
/** /**
@@ -30,8 +31,7 @@ export default class FSDProfile extends TranslatedComponent {
const ship = this.props.ship; const ship = this.props.ship;
this.state = { this.state = {
cargo: ship.cargoCapacity, calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel)
calcMaxRangeFunc: this._calcMaxRange.bind(this, ship)
}; };
} }
@@ -42,8 +42,8 @@ export default class FSDProfile extends TranslatedComponent {
* @return {boolean} Returns true if the component should be rerendered * @return {boolean} Returns true if the component should be rerendered
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.code != this.props.code) { if (nextProps.marker != this.props.marker) {
this.setState({ cargo: nextProps.ship.cargoCapacity, calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship) }); this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) });
} }
return true; return true;
} }
@@ -51,38 +51,23 @@ export default class FSDProfile extends TranslatedComponent {
/** /**
* Calculate the maximum range for this ship across its applicable mass * Calculate the maximum range for this ship across its applicable mass
* @param {Object} ship The ship * @param {Object} ship The ship
* @param {Object} fuel The fuel on the ship
* @param {Object} mass The mass at which to calculate the maximum range * @param {Object} mass The mass at which to calculate the maximum range
* @return {number} The maximum range * @return {number} The maximum range
*/ */
_calcMaxRange(ship, mass) { _calcMaxRange(ship, fuel, mass) {
// Obtain the FSD for this ship
const fsd = ship.standard[2].m;
// Obtain the maximum range // Obtain the maximum range
return Calc.jumpRange(mass, fsd, fsd.getMaxFuelPerJump()); return Calc.jumpRange(mass, ship.standard[2].m, Math.min(fuel, ship.standard[2].m.getMaxFuelPerJump()));
} }
/** /**
* Update cargo level * Render FSD profile
* @param {number} cargoLevel Cargo level 0 - 1
*/
_cargoChange(cargoLevel) {
let ship = this.props.ship;
let cargo = Math.round(ship.cargoCapacity * cargoLevel);
this.setState({
cargo
});
}
/**
* Render engine profile
* @return {React.Component} contents * @return {React.Component} contents
*/ */
render() { render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language; const { formats, translate, units } = language;
const { ship } = this.props; const { ship, cargo, fuel } = this.props;
const { cargo } = this.state;
// Calculate bounds for our line chart - use thruster info for X // Calculate bounds for our line chart - use thruster info for X
@@ -90,56 +75,30 @@ export default class FSDProfile extends TranslatedComponent {
const fsd = ship.standard[2].m; const fsd = ship.standard[2].m;
const minMass = ship.calcLowestPossibleMass({ th: thrusters }); const minMass = ship.calcLowestPossibleMass({ th: thrusters });
const maxMass = thrusters.getMaxMass(); const maxMass = thrusters.getMaxMass();
let mass = ship.unladenMass + ship.fuelCapacity + cargo; const mass = ship.unladenMass + fuel + cargo;
const minRange = Calc.jumpRange(maxMass, fsd, ship.fuelCapacity); const minRange = 0;
const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump()); const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump());
// Add a mark at our current mass // Add a mark at our current mass
const mark = Math.min(mass, maxMass); const mark = Math.min(mass, maxMass);
const cargoPercent = cargo / ship.cargoCapacity; const code = ship.name + ship.toString() + '.' + fuel;
const code = ship.name + ship.toString() + '.' + ship.getModificationsString() + '.' + ship.getPowerEnabledString();
return ( return (
<span> <LineChart
<h1>{translate('fsd profile')}</h1> xMin={minMass}
<LineChart xMax={maxMass}
width={this.props.chartWidth} yMin={minRange}
xMin={minMass} yMax={maxRange}
xMax={maxMass} xMark={mark}
yMin={minRange} xLabel={translate('mass')}
yMax={maxRange} xUnit={translate('T')}
xMark={mark} yLabel={translate('maximum range')}
xLabel={translate('mass')} yUnit={translate('LY')}
xUnit={translate('T')} func={this.state.calcMaxRangeFunc}
yLabel={translate('maximum range')} points={200}
yUnit={translate('LY')} code={code}
func={this.state.calcMaxRangeFunc} aspect={0.7}
points={200} />
code={code}
/>
{ship.cargoCapacity ?
<span>
<h3>{translate('cargo carried')}: {formats.int(cargo)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._cargoChange.bind(this)}
axisUnit={translate('T')}
percent={cargoPercent}
max={ship.cargoCapacity}
scale={sizeRatio}
onResize={onWindowResize}
/>
</td>
</tr>
</tbody>
</table>
</span> : '' }
</span>
); );
} }
} }

View File

@@ -0,0 +1,74 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import Slider from '../components/Slider';
/**
* Fuel slider
* Requires an onChange() function of the form onChange(fuel), providing the fuel in tonnes, which is triggered on fuel level change
*/
export default class Fuel extends TranslatedComponent {
static propTypes = {
fuel: React.PropTypes.number.isRequired,
fuelCapacity: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this._fuelChange = this._fuelChange.bind(this);
}
/**
* Update fuel level
* @param {number} fuelLevel percentage level from 0 to 1
*/
_fuelChange(fuelLevel) {
const { fuel, fuelCapacity } = this.props;
const newFuel = fuelLevel * fuelCapacity;
// Only send an update if the fuel has changed significantly
if (Math.round(fuel * 10) != Math.round(newFuel * 10)) {
this.props.onChange(Math.round(newFuel * 10) / 10);
}
}
/**
* Render fuel slider
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { fuel, fuelCapacity } = this.props;
return (
<span>
<h3>{translate('fuel carried')}: {formats.f1(fuel)}{units.T}</h3>
<table style={{ width: '100%', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody >
<tr>
<td>
<Slider
axis={true}
onChange={this._fuelChange}
axisUnit={translate('T')}
percent={fuel / fuelCapacity}
max={fuelCapacity}
scale={sizeRatio}
onResize={onWindowResize}
/>
</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -4,6 +4,7 @@ import Persist from '../stores/Persist';
import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons'; import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/** /**
@@ -51,6 +52,12 @@ export default class HardpointSlot extends Slot {
if (m.blueprint.special && m.blueprint.special.id >= 0) { if (m.blueprint.special && m.blueprint.special.id >= 0) {
modTT += ', ' + translate(m.blueprint.special.name); modTT += ', ' + translate(m.blueprint.special.name);
} }
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)}
</div>
);
} }
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}> return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
@@ -74,7 +81,7 @@ export default class HardpointSlot extends Slot {
{ m.getHps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'hpsshps' : 'hps')} onMouseOut={tooltip.bind(null, null)}>{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? <span>({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) })</span> : null }</div> : null } { m.getHps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'hpsshps' : 'hps')} onMouseOut={tooltip.bind(null, null)}>{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? <span>({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) })</span> : null }</div> : null }
{ m.getDps() && m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, 'dpe')} onMouseOut={tooltip.bind(null, null)}>{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}</div> : null } { m.getDps() && m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, 'dpe')} onMouseOut={tooltip.bind(null, null)}>{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}</div> : null }
{ m.getRoF() ? <div className={'l'} onMouseOver={termtip.bind(null, 'rof')} onMouseOut={tooltip.bind(null, null)}>{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}</div> : null } { m.getRoF() ? <div className={'l'} onMouseOver={termtip.bind(null, 'rof')} onMouseOut={tooltip.bind(null, null)}>{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}</div> : null }
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null } { m.getRange() ? <div className={'l'}>{translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null }
{ m.getScanTime() ? <div className={'l'}>{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}</div> : null } { m.getScanTime() ? <div className={'l'}>{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}</div> : null }
{ m.getFalloff() ? <div className={'l'}>{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}</div> : null } { m.getFalloff() ? <div className={'l'}>{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}</div> : null }
{ m.getShieldBoost() ? <div className={'l'}>+{formats.pct1(m.getShieldBoost())}</div> : null } { m.getShieldBoost() ? <div className={'l'}>+{formats.pct1(m.getShieldBoost())}</div> : null }

View File

@@ -356,7 +356,7 @@ export default class Header extends TranslatedComponent {
let comps = Object.keys(Persist.getComparisons()).sort(); let comps = Object.keys(Persist.getComparisons()).sort();
for (let name of comps) { for (let name of comps) {
comparisons.push(<ActiveLink key={name} href={'/compare/' + name} className='block name'>{name}</ActiveLink>); comparisons.push(<ActiveLink key={name} href={'/compare/' + encodeURIComponent(name)} className='block name'>{name}</ActiveLink>);
} }
} else { } else {
comparisons = <span className='cap'>{translate('none created')}</span>; comparisons = <span className='cap'>{translate('none created')}</span>;

View File

@@ -4,6 +4,7 @@ import Persist from '../stores/Persist';
import { ListModifications, Modified } from './SvgIcons'; import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/** /**
* Internal Slot * Internal Slot
@@ -30,6 +31,12 @@ export default class InternalSlot extends Slot {
let modTT = translate('modified'); let modTT = translate('modified');
if (m && m.blueprint && m.blueprint.name) { if (m && m.blueprint && m.blueprint.name) {
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)}
</div>
);
} }
let mass = m.getMass() || m.cargo || m.fuel || 0; let mass = m.getMass() || m.cargo || m.fuel || 0;

View File

@@ -13,9 +13,8 @@ import * as Calc from '../shipyard/Calculations';
* Jump range for a given ship * Jump range for a given ship
*/ */
export default class JumpRange extends TranslatedComponent { export default class JumpRange extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
chartWidth: React.PropTypes.number.isRequired,
code: React.PropTypes.string.isRequired code: React.PropTypes.string.isRequired
}; };
@@ -91,7 +90,6 @@ export default class JumpRange extends TranslatedComponent {
<span> <span>
<h1>{translate('jump range')}</h1> <h1>{translate('jump range')}</h1>
<LineChart <LineChart
width={this.props.chartWidth}
xMax={ship.cargoCapacity} xMax={ship.cargoCapacity}
yMax={ship.unladenRange} yMax={ship.unladenRange}
xLabel={translate('cargo')} xLabel={translate('cargo')}

View File

@@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import Measure from 'react-measure';
import * as d3 from 'd3'; import * as d3 from 'd3';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
@@ -14,11 +15,11 @@ export default class LineChart extends TranslatedComponent {
xMin: 0, xMin: 0,
yMin: 0, yMin: 0,
points: 20, points: 20,
colors: ['#ff8c0d'] colors: ['#ff8c0d'],
aspect: 0.5
}; };
static PropTypes = { static propTypes = {
width: React.PropTypes.number.isRequired,
func: React.PropTypes.func.isRequired, func: React.PropTypes.func.isRequired,
xLabel: React.PropTypes.string.isRequired, xLabel: React.PropTypes.string.isRequired,
xMin: React.PropTypes.number, xMin: React.PropTypes.number,
@@ -32,6 +33,7 @@ export default class LineChart extends TranslatedComponent {
series: React.PropTypes.array, series: React.PropTypes.array,
colors: React.PropTypes.array, colors: React.PropTypes.array,
points: React.PropTypes.number, points: React.PropTypes.number,
aspect: React.PropTypes.number,
code: React.PropTypes.string, code: React.PropTypes.string,
}; };
@@ -63,7 +65,11 @@ export default class LineChart extends TranslatedComponent {
xScale, xScale,
xAxisScale, xAxisScale,
yScale, yScale,
tipHeight: 2 + (1.2 * (series ? series.length : 0.8)) tipHeight: 2 + (1.2 * (series ? series.length : 0.8)),
dimensions: {
width: 100,
height: 100
}
}; };
} }
@@ -73,13 +79,14 @@ export default class LineChart extends TranslatedComponent {
*/ */
_tooltip(xPos) { _tooltip(xPos) {
let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props;
let { xScale, yScale, innerWidth } = this.state; let { xScale, yScale } = this.state;
let { width } = this.state.dimensions;
let { formats, translate } = this.context.language; let { formats, translate } = this.context.language;
let x0 = xScale.invert(xPos), let x0 = xScale.invert(xPos),
y0 = func(x0), y0 = func(x0),
tips = this.tipContainer, tips = this.tipContainer,
yTotal = 0, yTotal = 0,
flip = (xPos / innerWidth > 0.60), flip = (xPos / width > 0.50),
tipWidth = 0, tipWidth = 0,
tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height;
@@ -110,19 +117,21 @@ export default class LineChart extends TranslatedComponent {
/** /**
* Update dimensions based on properties and scale * Update dimensions based on properties and scale
* @param {Object} props React Component properties * @param {Object} props React Component properties
* @param {number} scale size ratio / scale * @param {number} scale size ratio / scale
* @returns {Object} calculated dimensions
*/ */
_updateDimensions(props, scale) { _updateDimensions(props, scale) {
let { width, xMax, xMin, yMin, yMax } = props; const { xMax, xMin, yMin, yMax } = props;
let innerWidth = width - MARGIN.left - MARGIN.right; const { width, height } = this.state.dimensions;
let outerHeight = Math.round(width * 0.5 * scale); const innerWidth = width - MARGIN.left - MARGIN.right;
let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; const outerHeight = Math.round(width * props.aspect);
const innerHeight = outerHeight - MARGIN.top - MARGIN.bottom;
this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true);
this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true);
this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility
this.setState({ innerWidth, outerHeight, innerHeight }); return { innerWidth, outerHeight, innerHeight };
} }
/** /**
@@ -183,7 +192,7 @@ export default class LineChart extends TranslatedComponent {
for (let i = 0, l = series ? series.length : 1; i < l; i++) { for (let i = 0, l = series ? series.length : 1; i < l; i++) {
const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]);
seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor));
detailElems.push(<text key={i} className='text-tip y' stroke={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>); detailElems.push(<text key={i} className='text-tip y' strokeWidth={0} fill={props.colors[i]} y={1.25 * (i + 2) + 'em'}/>);
markerElems.push(<circle key={i} className='marker' r='4' />); markerElems.push(<circle key={i} className='marker' r='4' />);
} }
@@ -196,7 +205,6 @@ export default class LineChart extends TranslatedComponent {
* Update dimensions and series data based on props and context. * Update dimensions and series data based on props and context.
*/ */
componentWillMount() { componentWillMount() {
this._updateDimensions(this.props, this.context.sizeRatio);
this._updateSeries(this.props, this.state); this._updateSeries(this.props, this.state);
} }
@@ -206,14 +214,7 @@ export default class LineChart extends TranslatedComponent {
* @param {Object} nextContext Incoming/Next conext * @param {Object} nextContext Incoming/Next conext
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
let { func, xMin, xMax, yMin, yMax, width } = nextProps; const props = this.props;
let props = this.props;
let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func;
if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) {
this._updateDimensions(nextProps, nextContext.sizeRatio);
}
if (props.code != nextProps.code) { if (props.code != nextProps.code) {
this._updateSeries(nextProps, this.state); this._updateSeries(nextProps, this.state);
@@ -225,53 +226,57 @@ export default class LineChart extends TranslatedComponent {
* @return {React.Component} Chart SVG * @return {React.Component} Chart SVG
*/ */
render() { render() {
if (!this.props.width) { const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio);
return null; const { width, height } = this.state.dimensions;
} const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props;
const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state;
let { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; const line = this.line;
let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; const lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
let line = this.line;
let lines = seriesLines.map((line, i) => <path key={i} className='line' fill='none' stroke={colors[i]} strokeWidth='1' d={line(seriesData)} />).reverse();
const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0;
const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={colors[0]} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : ''; const xmark = xMark ? <path key={'mark'} className='line' fill='none' strokeDasharray='5,5' stroke={'#ff8c0d'} strokeWidth='1' d={'M ' + markX + ' ' + innerHeight + ' L ' + markX + ' 0'} /> : '';
return <svg style={{ width: '100%', height: outerHeight }}> return (
<g transform={`translate(${MARGIN.left},${MARGIN.top})`}> <Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
<g>{xmark}</g> <div width={width} height={height}>
<g>{lines}</g> <svg style={{ width: '100%', height: outerHeight }}>
<g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> <g transform={`translate(${MARGIN.left},${MARGIN.top})`}>
<text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}> <g>{xmark}</g>
<tspan>{xLabel}</tspan> <g>{lines}</g>
<tspan className='metric'> ({xUnit})</tspan> <g className='x axis' ref={(elem) => d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}>
</text> <text className='cap' y='30' dy='.1em' x={innerWidth / 2} style={{ textAnchor: 'middle' }}>
</g> <tspan>{xLabel}</tspan>
<g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}> <tspan className='metric'> ({xUnit})</tspan>
<text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}> </text>
<tspan>{yLabel}</tspan> </g>
{ yUnit && <tspan className='metric'> ({yUnit})</tspan> } <g className='y axis' ref={(elem) => d3.select(elem).call(this.yAxis)}>
</text> <text className='cap' transform='rotate(-90)' y='-50' dy='.1em' x={innerHeight / -2} style={{ textAnchor: 'middle' }}>
</g> <tspan>{yLabel}</tspan>
<g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}> { yUnit && <tspan className='metric'> ({yUnit})</tspan> }
<rect className='tooltip' height={tipHeight + 'em'}></rect> </text>
{detailElems} </g>
</g> <g ref={(g) => this.tipContainer = d3.select(g)} style={{ display: 'none' }}>
<g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}> <rect className='tooltip' height={tipHeight + 'em'}></rect>
{markerElems} {detailElems}
</g> </g>
<rect <g ref={(g) => this.markersContainer = d3.select(g)} style={{ display: 'none' }}>
fillOpacity='0' {markerElems}
height={innerHeight} </g>
width={innerWidth + 1} <rect
onMouseEnter={this._showTip} fillOpacity='0'
onTouchStart={this._showTip} height={innerHeight}
onMouseLeave={this._hideTip} width={innerWidth + 1}
onTouchEnd={this._hideTip} onMouseEnter={this._showTip}
onMouseMove={this._moveTip} onTouchStart={this._showTip}
onTouchMove={this._moveTip} onMouseLeave={this._hideTip}
/> onTouchEnd={this._hideTip}
</g> onMouseMove={this._moveTip}
</svg>; onTouchMove={this._moveTip}
/>
</g>
</svg>
</div>
</Measure>
);
} }
} }

View File

@@ -5,6 +5,7 @@ import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions';
import cn from 'classnames'; import cn from 'classnames';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import Modification from './Modification'; import Modification from './Modification';
import { getBlueprint, blueprintTooltip } from '../utils/BlueprintFunctions';
/** /**
* Modifications menu * Modifications menu
@@ -43,16 +44,18 @@ export default class ModificationsMenu extends TranslatedComponent {
*/ */
_initState(props, context) { _initState(props, context) {
let { m } = props; let { m } = props;
const { language } = context; const { language, tooltip, termtip } = context;
const translate = language.translate; const translate = language.translate;
// Set up the blueprints // Set up the blueprints
let blueprints = []; let blueprints = [];
for (const blueprintName in Modifications.modules[m.grp].blueprints) { for (const blueprintName in Modifications.modules[m.grp].blueprints) {
for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) { for (const grade of Modifications.modules[m.grp].blueprints[blueprintName]) {
const close = this._blueprintSelected.bind(this, Modifications.blueprints[blueprintName].id, grade); const close = this._blueprintSelected.bind(this, blueprintName, grade);
const key = blueprintName + ':' + grade; const key = blueprintName + ':' + grade;
blueprints.push(<div style={{ cursor: 'pointer' }} key={ key } onClick={ close }>{translate(Modifications.blueprints[blueprintName].name + ' grade ' + grade)}</div>); const blueprint = getBlueprint(blueprintName, m);
const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade].features);
blueprints.push(<div style={{ cursor: 'pointer' }} key={ key } onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={ close }>{translate(blueprint.name + ' grade ' + grade)}</div>);
} }
} }
@@ -103,12 +106,12 @@ export default class ModificationsMenu extends TranslatedComponent {
/** /**
* Activated when a blueprint is selected * Activated when a blueprint is selected
* @param {int} blueprintId The ID of the selected blueprint * @param {int} fdname The Frontier name of the blueprint
* @param {int} grade The grade of the selected blueprint * @param {int} grade The grade of the selected blueprint
*/ */
_blueprintSelected(blueprintId, grade) { _blueprintSelected(fdname, grade) {
const { m } = this.props; const { m } = this.props;
const blueprint = Object.assign({}, _.find(Modifications.blueprints, function(o) { return o.id === blueprintId; })); const blueprint = getBlueprint(fdname, m);
blueprint.grade = grade; blueprint.grade = grade;
m.blueprint = blueprint; m.blueprint = blueprint;
@@ -153,13 +156,6 @@ export default class ModificationsMenu extends TranslatedComponent {
* @param {number} value The value of the roll * @param {number} value The value of the roll
*/ */
_setRollResult(ship, m, featureName, value) { _setRollResult(ship, m, featureName, value) {
if (Modifications.modifications[featureName].method !== 'overwrite') {
if (m.grp == 'sb' && featureName == 'shieldboost') {
// Shield boosters are a special case. Their boost is dependent on their base so we need to calculate the value here
value = ((1 + m.shieldboost) * (1 + value) - 1) / m.shieldboost - 1;
}
}
if (Modifications.modifications[featureName].type == 'percentage') { if (Modifications.modifications[featureName].type == 'percentage') {
ship.setModification(m, featureName, value * 10000); ship.setModification(m, featureName, value * 10000);
} else if (Modifications.modifications[featureName].type == 'numeric') { } else if (Modifications.modifications[featureName].type == 'numeric') {
@@ -276,11 +272,11 @@ export default class ModificationsMenu extends TranslatedComponent {
let blueprintLabel; let blueprintLabel;
let haveBlueprint = false; let haveBlueprint = false;
let blueprintTt;
if (m.blueprint && !isEmpty(m.blueprint)) { if (m.blueprint && !isEmpty(m.blueprint)) {
blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
haveBlueprint = true; haveBlueprint = true;
} else { blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features);
blueprintLabel = translate('PHRASE_SELECT_BLUEPRINT');
} }
let specialLabel; let specialLabel;
@@ -291,7 +287,7 @@ export default class ModificationsMenu extends TranslatedComponent {
} }
const showBlueprintsMenu = blueprintMenuOpened; const showBlueprintsMenu = blueprintMenuOpened;
const showSpecial = haveBlueprint && this.state.specials.length > 0; const showSpecial = haveBlueprint && this.state.specials.length > 0 && !blueprintMenuOpened;
const showSpecialsMenu = specialMenuOpened; const showSpecialsMenu = specialMenuOpened;
const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened; const showRolls = haveBlueprint && !blueprintMenuOpened && !specialMenuOpened;
const showReset = !blueprintMenuOpened && !specialMenuOpened; const showReset = !blueprintMenuOpened && !specialMenuOpened;
@@ -303,7 +299,10 @@ export default class ModificationsMenu extends TranslatedComponent {
onClick={(e) => e.stopPropagation() } onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation} onContextMenu={stopCtxPropagation}
> >
<div className={ cn('section-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu}>{blueprintLabel}</div> { haveBlueprint ?
<div className={ cn('section-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu}>{blueprintLabel}</div>
:
<div className={ cn('section-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
{ showBlueprintsMenu ? this.state.blueprints : null } { showBlueprintsMenu ? this.state.blueprints : null }
{ showSpecial ? <div className={ cn('section-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleSpecialsMenu}>{specialLabel}</div> : null } { showSpecial ? <div className={ cn('section-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleSpecialsMenu}>{specialLabel}</div> : null }
{ showSpecialsMenu ? this.state.specials : null } { showSpecialsMenu ? this.state.specials : null }

View File

@@ -0,0 +1,70 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
/**
* Movement
*/
export default class Movement extends TranslatedComponent {
static propTypes = {
marker: React.PropTypes.string.isRequired,
ship: React.PropTypes.object.isRequired,
boost: React.PropTypes.bool.isRequired,
eng: React.PropTypes.number.isRequired,
fuel: React.PropTypes.number.isRequired,
cargo: React.PropTypes.number.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
}
/**
* Render movement
* @return {React.Component} contents
*/
render() {
const { ship, boost, eng, cargo, fuel } = this.props;
const { language } = this.context;
const { formats, translate, units } = language;
return (
<span id='movement'>
<svg viewBox='0 0 600 600' fillRule="evenodd" clipRule="evenodd">
// Axes
<path d="M150 250v300" strokeWidth='1'/>
<path d="M150 250l236 236" strokeWidth='1'/>
<path d="M150 250l350 -200" strokeWidth='1'/>
// End Arrow
<path d="M508 43.3L487 67l-10-17.3 31-6.4z"/>
// Axes arcs and arrows
<path d="M71.7 251.7C64.2 259.2 60 269.4 60 280c0 22 18 40 40 40s40-18 40-40c0-10.6-4.2-20.8-11.7-28.3 7.5 7.5 11.7 17.7 11.7 28.3 0 22-18 40-40 40s-40-18-40-40c0-10.6 4.2-20.8 11.7-28.3z" strokeWidth='4' transform="matrix(.6 0 0 .3 87.5 376.3)"/>
<path d="M142.8 453l-13.2 8.7-2.6-9.7 15.8 1z"/>
<path d="M144.7 451.6l.5 1.6-16.2 10.6h-.4l-3.5-13 .7-.4 19.3 1.2zm-14.2 7.7l7.7-5-9.2-.7 1.5 5.7zm25.7-6.3l15.8-1-2.6 9.7-13.2-8.8z"/>
<path d="M174 450.8l-3.6 13h-.4l-16.2-10.6.5-1.6 19.3-1.2.3.4zm-13.2 3.4l7.7 5 1.5-5.6-9.2.6z"/>
<path d="M407.7 119c2 .7 4.3 1 6.4 1 14 0 25-11.2 25-25s-11-25-25-25c-11 0-21 7.6-24 18.5 3-11 13-18.5 24-18.5 14 0 25 11.2 25 25s-11 25-25 25c-2 0-4-.3-6-1z" strokeWidth='2'/>
<path d="M388 99.7L387 84l9.8 2.5-8.7 13.2z"/>
<path d="M398.8 85.5l.2.5-10.7 16-1.6-.3-1.2-19.3.4-.3 12.5 3.8zm-9.5 9.7l5-7.7-5.6-1.6.6 9zm10 20.8l15.7-1-2.6 9.7-13.2-8.8z"/>
<path d="M417 113.8l-3.6 13h-.4l-16.2-10.6.5-1.6 19.3-1.2.3.4zm-13.2 3.4l7.7 5 1.5-5.6-9.2.6z"/>
<path d="M355 430c0-13.8-11.2-25-25-25s-25 11.2-25 25 11.2 25 25 25c-13.8 0-25-11.2-25-25s11.2-25 25-25 25 11.2 25 25z" strokeWidth='2'/>
<path d="M357 439.7l-8.8-13 9.7-2.7-1 15.7z"/>
<path d="M359.5 422.4l-1.2 19.3-1.6.4-10.7-16 .2-.2 13-3.4.3.4zm-9 5l5.2 7.8.6-9.3-5.7 1.2zm-10.5 24l-13.2 8.6-2.6-9.7 15.8 1z"/>
<path d="M342 450l.4 1.5-16.2 10.7-.4-.2-3.5-13 .3-.3L342 450zm-14.3 7.6l7.7-5-9.2-.6 1.5 5.6z"/>
// Speed
<text x="470" y="30" strokeWidth='0'>{formats.int(ship.calcSpeed(eng, fuel, cargo, boost))}m/s</text>
// Pitch
<text x="355" y="410" strokeWidth='0'>{formats.int(ship.calcPitch(eng, fuel, cargo, boost))}°/s</text>
// Roll
<text x="450" y="110" strokeWidth='0'>{formats.int(ship.calcRoll(eng, fuel, cargo, boost))}°/s</text>
// Yaw
<text x="160" y="430" strokeWidth='0'>{formats.int(ship.calcYaw(eng, fuel, cargo, boost))}°/s</text>
</svg>
</span>);
}
}

View File

@@ -6,7 +6,7 @@ import TranslatedComponent from './TranslatedComponent';
* Movement summary * Movement summary
*/ */
export default class MovementSummary extends TranslatedComponent { export default class MovementSummary extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired ship: React.PropTypes.object.isRequired
}; };

View File

@@ -0,0 +1,264 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations';
import PieChart from './PieChart';
import { nameComparator } from '../utils/SlotFunctions';
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
import VerticalBarChart from './VerticalBarChart';
/**
* 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);
};
}
/**
* Offence information
* Offence information consists of four panels:
* - textual information (time to drain cap, time to take down shields etc.)
* - breakdown of damage sources (pie chart)
* - comparison of shield resistances (table chart)
* - effective sustained DPS of weapons (bar chart)
*/
export default class Offence extends TranslatedComponent {
static propTypes = {
marker: React.PropTypes.string.isRequired,
ship: React.PropTypes.object.isRequired,
opponent: React.PropTypes.object.isRequired,
engagementrange: React.PropTypes.number.isRequired,
wep: React.PropTypes.number.isRequired,
opponentSys: React.PropTypes.number.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this._sort = this._sort.bind(this);
const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange);
this.state = {
predicate: 'n',
desc: true,
damage
};
}
/**
* Update the state if our properties change
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) {
const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange);
this.setState({ damage });
}
return true;
}
/**
* Set the sort order and sort
* @param {string} predicate Sort predicate
*/
_sortOrder(predicate) {
let desc = this.state.desc;
if (predicate == this.state.predicate) {
desc = !desc;
} else {
desc = true;
}
this._sort(predicate, desc);
this.setState({ predicate, desc });
}
/**
* Sorts the weapon list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort order descending
*/
_sort(predicate, desc) {
let comp = weaponComparator.bind(null, this.context.language.translate);
switch (predicate) {
case 'n': comp = comp(null, desc); break;
case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break;
case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break;
case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break;
case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break;
}
this.state.damage.sort(comp);
}
/**
* Render offence
* @return {React.Component} contents
*/
render() {
const { ship, opponent, wep, engagementrange } = this.props;
const { language, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { damage } = this.state;
const sortOrder = this._sortOrder;
const pd = ship.standard[4].m;
const opponentShields = Calc.shieldMetrics(opponent, 4);
const opponentArmour = Calc.armourMetrics(opponent);
const timeToDrain = Calc.timeToDrainWep(ship, wep);
let absoluteShieldsSDps = 0;
let explosiveShieldsSDps = 0;
let kineticShieldsSDps = 0;
let thermalShieldsSDps = 0;
let absoluteArmourSDps = 0;
let explosiveArmourSDps = 0;
let kineticArmourSDps = 0;
let thermalArmourSDps = 0;
let totalSEps = 0;
const rows = [];
for (let i = 0; i < damage.length; i++) {
const weapon = damage[i];
totalSEps += weapon.seps;
absoluteShieldsSDps += weapon.sdps.shields.absolute;
explosiveShieldsSDps += weapon.sdps.shields.explosive;
kineticShieldsSDps += weapon.sdps.shields.kinetic;
thermalShieldsSDps += weapon.sdps.shields.thermal;
absoluteArmourSDps += weapon.sdps.armour.absolute;
explosiveArmourSDps += weapon.sdps.armour.explosive;
kineticArmourSDps += weapon.sdps.armour.kinetic;
thermalArmourSDps += weapon.sdps.armour.thermal;
const effectivenessShieldsTooltipDetails = [];
effectivenessShieldsTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}</div>);
effectivenessShieldsTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}</div>);
effectivenessShieldsTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}</div>);
const effectiveShieldsSDpsTooltipDetails = [];
if (weapon.sdps.shields.absolute) effectiveShieldsSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.shields.absolute)}</div>);
if (weapon.sdps.shields.explosive) effectiveShieldsSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.shields.explosive)}</div>);
if (weapon.sdps.shields.kinetic) effectiveShieldsSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.shields.kinetic)}</div>);
if (weapon.sdps.shields.thermal) effectiveShieldsSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.shields.thermal)}</div>);
const effectivenessArmourTooltipDetails = [];
effectivenessArmourTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}</div>);
effectivenessArmourTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}</div>);
effectivenessArmourTooltipDetails.push(<div key='hardness'>{translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}</div>);
const effectiveArmourSDpsTooltipDetails = [];
if (weapon.sdps.armour.absolute) effectiveArmourSDpsTooltipDetails.push(<div key='absolute'>{translate('absolute') + ' ' + formats.f1(weapon.sdps.armour.absolute)}</div>);
if (weapon.sdps.armour.explosive) effectiveArmourSDpsTooltipDetails.push(<div key='explosive'>{translate('explosive') + ' ' + formats.f1(weapon.sdps.armour.explosive)}</div>);
if (weapon.sdps.armour.kinetic) effectiveArmourSDpsTooltipDetails.push(<div key='kinetic'>{translate('kinetic') + ' ' + formats.f1(weapon.sdps.armour.kinetic)}</div>);
if (weapon.sdps.armour.thermal) effectiveArmourSDpsTooltipDetails.push(<div key='thermal'>{translate('thermal') + ' ' + formats.f1(weapon.sdps.armour.thermal)}</div>);
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)}
{weapon.engineering ? ' (' + weapon.engineering + ')' : null }
</td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessShieldsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.armour.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessArmourTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.armour.total)}</span></td>
</tr>);
}
const totalShieldsSDps = absoluteShieldsSDps + explosiveShieldsSDps + kineticShieldsSDps + thermalShieldsSDps;
const totalArmourSDps = absoluteArmourSDps + explosiveArmourSDps + kineticArmourSDps + thermalArmourSDps;
const shieldsSDpsData = [];
shieldsSDpsData.push({ value: Math.round(absoluteShieldsSDps), label: translate('absolute') });
shieldsSDpsData.push({ value: Math.round(explosiveShieldsSDps), label: translate('explosive') });
shieldsSDpsData.push({ value: Math.round(kineticShieldsSDps), label: translate('kinetic') });
shieldsSDpsData.push({ value: Math.round(thermalShieldsSDps), label: translate('thermal') });
const armourSDpsData = [];
armourSDpsData.push({ value: Math.round(absoluteArmourSDps), label: translate('absolute') });
armourSDpsData.push({ value: Math.round(explosiveArmourSDps), label: translate('explosive') });
armourSDpsData.push({ value: Math.round(kineticArmourSDps), label: translate('kinetic') });
armourSDpsData.push({ value: Math.round(thermalArmourSDps), label: translate('thermal') });
const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
return (
<span id='offence'>
<div className='group full'>
<table>
<thead>
<tr className='main'>
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</th>
<th colSpan='2'>{translate('opponent\`s shields')}</th>
<th colSpan='2'>{translate('opponent\`s armour')}</th>
</tr>
<tr>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'es')}>{'eft'}</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'esdpsh')}>{'sdps'}</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'eh')}>{'eft'}</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>
<div className='group quarter'>
<h2>{translate('offence metrics')}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}</h2>
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('armour metrics'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield damage sources')}</h2>
<PieChart data={shieldsSDpsData} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
<PieChart data={armourSDpsData} />
</div>
</span>);
}
}

View File

@@ -7,7 +7,7 @@ import { DamageAbsolute, DamageKinetic, DamageThermal, DamageExplosive } from '.
* Offence summary * Offence summary
*/ */
export default class OffenceSummary extends TranslatedComponent { export default class OffenceSummary extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired ship: React.PropTypes.object.isRequired
}; };

View File

@@ -0,0 +1,186 @@
import React from 'react';
import cn from 'classnames';
import { Ships } from 'coriolis-data/dist';
import Ship from '../shipyard/Ship';
import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent';
import PowerManagement from './PowerManagement';
import CostSection from './CostSection';
import EngineProfile from './EngineProfile';
import FSDProfile from './FSDProfile';
import Movement from './Movement';
import Offence from './Offence';
import Defence from './Defence';
import WeaponDamageChart from './WeaponDamageChart';
/**
* Outfitting subpages
*/
export default class OutfittingSubpages extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
code: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired,
buildName: React.PropTypes.string,
sys: React.PropTypes.number.isRequired,
eng: React.PropTypes.number.isRequired,
wep: React.PropTypes.number.isRequired,
cargo: React.PropTypes.number.isRequired,
fuel: React.PropTypes.number.isRequired,
boost: React.PropTypes.bool.isRequired,
engagementRange: React.PropTypes.number.isRequired,
opponent: React.PropTypes.object.isRequired,
opponentBuild: React.PropTypes.string,
opponentSys: React.PropTypes.number.isRequired,
opponentEng: React.PropTypes.number.isRequired,
opponentWep: React.PropTypes.number.isRequired,
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this._powerTab = this._powerTab.bind(this);
this._profilesTab = this._profilesTab.bind(this);
this._offenceTab = this._offenceTab.bind(this);
this._defenceTab = this._defenceTab.bind(this);
this.state = {
tab: Persist.getOutfittingTab() || 'power',
};
}
/**
* Show selected tab
* @param {string} tab Tab name
*/
_showTab(tab) {
this.setState({ tab });
}
/**
* Render the power tab
* @return {React.Component} Tab contents
*/
_powerTab() {
let { ship, buildName, code, onChange } = this.props;
Persist.setOutfittingTab('power');
const powerMarker = `${ship.toString()}`;
const costMarker = `${ship.toString().split('.')[0]}`;
return <div>
<PowerManagement ship={ship} code={powerMarker} onChange={onChange} />
<CostSection ship={ship} buildName={buildName} code={costMarker} />
</div>;
}
/**
* Render the profiles tab
* @return {React.Component} Tab contents
*/
_profilesTab() {
const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props;
const { translate } = this.context.language;
let realBoost = boost && ship.canBoost();
Persist.setOutfittingTab('profiles');
const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`;
const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`;
const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost()}`;
const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`;
return <div>
<div className='group third'>
<h1>{translate('engine profile')}</h1>
<EngineProfile ship={ship} marker={engineProfileMarker} fuel={fuel} cargo={cargo} eng={eng} boost={realBoost} />
</div>
<div className='group third'>
<h1>{translate('fsd profile')}</h1>
<FSDProfile ship={ship} marker={fsdProfileMarker} fuel={fuel} cargo={cargo} />
</div>
<div className='group third'>
<h1>{translate('movement profile')}</h1>
<Movement marker={movementMarker} ship={ship} boost={boost} eng={eng} cargo={cargo} fuel={fuel} />
</div>
<div className='group half'>
<h1>{translate('damage to opponent\'s shields')}</h1>
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={false} engagementRange={engagementRange} />
</div>
<div className='group half'>
<h1>{translate('damage to opponent\'s hull')}</h1>
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={true} engagementRange={engagementRange} />
</div>
</div>;
}
/**
* Render the offence tab
* @return {React.Component} Tab contents
*/
_offenceTab() {
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props;
Persist.setOutfittingTab('offence');
const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`;
return <div>
<Offence marker={marker} ship={ship} opponent={opponent} wep={wep} opponentSys={opponentSys} engagementrange={engagementRange}/>
</div>;
}
/**
* Render the defence tab
* @return {React.Component} Tab contents
*/
_defenceTab() {
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props;
Persist.setOutfittingTab('defence');
const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`;
return <div>
<Defence marker={marker} ship={ship} opponent={opponent} sys={sys} opponentWep={opponentWep} engagementrange={engagementRange}/>
</div>;
}
/**
* Render the section
* @return {React.Component} Contents
*/
render() {
const tab = this.state.tab;
const translate = this.context.language.translate;
let tabSection;
switch (tab) {
case 'power': tabSection = this._powerTab(); break;
case 'profiles': tabSection = this._profilesTab(); break;
case 'offence': tabSection = this._offenceTab(); break;
case 'defence': tabSection = this._defenceTab(); break;
}
return (
<div className='group full' style={{ minHeight: '1000px' }}>
<table className='tabs'>
<thead>
<tr>
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })} onClick={this._showTab.bind(this, 'power')} >{translate('power and costs')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })} onClick={this._showTab.bind(this, 'profiles')} >{translate('profiles')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })} onClick={this._showTab.bind(this, 'offence')} >{translate('offence')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })} onClick={this._showTab.bind(this, 'defence')} >{translate('defence')}</th>
</tr>
</thead>
</table>
{tabSection}
</div>
);
}
}

View File

@@ -0,0 +1,96 @@
import React, { Component } from 'react';
import Measure from 'react-measure';
import * as d3 from 'd3';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000';
/**
* A pie chart
*/
export default class PieChart extends Component {
static propTypes = {
data : React.PropTypes.array.isRequired
};
/**
* 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
* @param {Object} d the data for this slice
* @param {number} i the index of this slice
* @returns {Object} the SVG for the slice
*/
sliceGenerator(d, i) {
if (!d || d.value == 0) {
// Ignore 0 values
return null;
}
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 nonZeroItems = data.filter(d => d.value != 0).length;
const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1;
const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5);
const keyTranslate = `translate(${keyX}, ${width * 0.45})`;
return (
<g key={`group-${i}`}>
<path key={`arc-${i}`} d={this.arc(d)} style={{ fill: this.colors[i] }} />
<text key={`label-${i}`} transform={labelTranslate} style={{ strokeWidth: '0px', fill: LABEL_COLOUR }} textAnchor='middle'>{d.value}</text>
<text key={`key-${i}`} transform={keyTranslate} style={{ strokeWidth:'0px', fill: this.colors[i] }} textAnchor='middle'>{d.data.label}</text>
</g>
);
}
/**
* Render the component
* @returns {object} Markup
*/
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 (
<Measure width='100%' whitelist={['width', 'top']} onMeasure={ (dimensions) => { this.setState({ dimensions }); }}>
<div width={width} height={width}>
<svg style={{ stroke: 'None' }} width={width} height={width * 0.9}>
<g transform={translate}>
{pie.map((d, i) => this.sliceGenerator(d, i))}
</g>
</svg>
</div>
</Measure>
);
}
}

299
src/app/components/Pips.jsx Normal file
View File

@@ -0,0 +1,299 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import ShipSelector from './ShipSelector';
import { nameComparator } from '../utils/SlotFunctions';
import { Pip } from './SvgIcons';
import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import Module from '../shipyard/Module';
/**
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
* Requires an onChange() function of the form onChange(sys, eng, wep) which is triggered whenever the pips change.
*/
export default class Pips extends TranslatedComponent {
static propTypes = {
sys: React.PropTypes.number.isRequired,
eng: React.PropTypes.number.isRequired,
wep: React.PropTypes.number.isRequired,
onChange: React.PropTypes.func.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
const { sys, eng, wep } = props;
this._keyDown = this._keyDown.bind(this);
}
/**
* Add listeners after mounting
*/
componentDidMount() {
document.addEventListener('keydown', this._keyDown);
}
/**
* Remove listeners before unmounting
*/
componentWillUnmount() {
document.removeEventListener('keydown', this._keyDown);
}
/**
* Handle Key Down
* @param {Event} e Keyboard Event
*/
_keyDown(e) {
if (e.ctrlKey || e.metaKey) { // CTRL/CMD
switch (e.keyCode) {
case 37: // Left arrow == increase SYS
e.preventDefault();
this._incSys();
break;
case 38: // Up arrow == increase ENG
e.preventDefault();
this._incEng();
break;
case 39: // Right arrow == increase WEP
e.preventDefault();
this._incWep();
break;
case 40: // Down arrow == reset
e.preventDefault();
this._reset();
break;
}
}
}
/**
* Handle a click
* @param {string} which Which item was clicked
*/
onClick(which) {
if (which == 'SYS') {
this._incSys();
} else if (which == 'ENG') {
this._incEng();
} else if (which == 'WEP') {
this._incWep();
} else if (which == 'RST') {
this._reset();
}
}
/**
* Reset the capacitor
*/
_reset() {
let { sys, eng, wep } = this.props;
if (sys != 2 || eng != 2 || wep != 2) {
sys = eng = wep = 2;
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the SYS capacitor
*/
_incSys() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - sys);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (eng > wep) {
eng -= 0.5;
sys += 0.5;
} else {
wep -= 0.5;
sys += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (eng == 0) {
wep -= 1;
sys += 1;
} else if (wep == 0) {
eng -= 1;
sys += 1;
} else {
eng -= 0.5;
wep -= 0.5;
sys += 1;
}
}
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the ENG capacitor
*/
_incEng() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - eng);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (sys > wep) {
sys -= 0.5;
eng += 0.5;
} else {
wep -= 0.5;
eng += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (sys == 0) {
wep -= 1;
eng += 1;
} else if (wep == 0) {
sys -= 1;
eng += 1;
} else {
sys -= 0.5;
wep -= 0.5;
eng += 1;
}
}
this.props.onChange(sys, eng, wep);
}
}
/**
* Increment the WEP capacitor
*/
_incWep() {
let { sys, eng, wep } = this.props;
const required = Math.min(1, 4 - wep);
if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (sys > eng) {
sys -= 0.5;
wep += 0.5;
} else {
eng -= 0.5;
wep += 0.5;
}
} else {
// Required is 1 - take from both if possible
if (sys == 0) {
eng -= 1;
wep += 1;
} else if (eng == 0) {
sys -= 1;
wep += 1;
} else {
sys -= 0.5;
eng -= 0.5;
wep += 1;
}
}
this.props.onChange(sys, eng, wep);
}
}
/**
* Set up the rendering for pips
* @param {int} sys the SYS pips
* @param {int} eng the ENG pips
* @param {int} wep the WEP pips
* @returns {Object} Object containing the rendering for the pips
*/
_renderPips(sys, eng, wep) {
const pipsSvg = {};
// SYS
pipsSvg['SYS'] = [];
for (let i = 0; i < Math.floor(sys); i++) {
pipsSvg['SYS'].push(<Pip className='full' key={i} />);
}
if (sys > Math.floor(sys)) {
pipsSvg['SYS'].push(<Pip className='half' key={'half'} />);
}
for (let i = Math.floor(sys + 0.5); i < 4; i++) {
pipsSvg['SYS'].push(<Pip className='empty' key={i} />);
}
// ENG
pipsSvg['ENG'] = [];
for (let i = 0; i < Math.floor(eng); i++) {
pipsSvg['ENG'].push(<Pip className='full' key={i} />);
}
if (eng > Math.floor(eng)) {
pipsSvg['ENG'].push(<Pip className='half' key={'half'} />);
}
for (let i = Math.floor(eng + 0.5); i < 4; i++) {
pipsSvg['ENG'].push(<Pip className='empty' key={i} />);
}
// WEP
pipsSvg['WEP'] = [];
for (let i = 0; i < Math.floor(wep); i++) {
pipsSvg['WEP'].push(<Pip className='full' key={i} />);
}
if (wep > Math.floor(wep)) {
pipsSvg['WEP'].push(<Pip className='half' key={'half'} />);
}
for (let i = Math.floor(wep + 0.5); i < 4; i++) {
pipsSvg['WEP'].push(<Pip className='empty' key={i} />);
}
return pipsSvg;
}
/**
* Render pips
* @return {React.Component} contents
*/
render() {
const { formats, translate, units } = this.context.language;
const { sys, eng, wep } = this.props;
const onSysClicked = this.onClick.bind(this, 'SYS');
const onEngClicked = this.onClick.bind(this, 'ENG');
const onWepClicked = this.onClick.bind(this, 'WEP');
const onRstClicked = this.onClick.bind(this, 'RST');
const pipsSvg = this._renderPips(sys, eng, wep);
return (
<span id='pips'>
<table>
<tbody>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td className='clickable' onClick={onEngClicked}>{pipsSvg['ENG']}</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td className='clickable' onClick={onSysClicked}>{pipsSvg['SYS']}</td>
<td className='clickable' onClick={onEngClicked}>{translate('ENG')}</td>
<td className='clickable' onClick={onWepClicked}>{pipsSvg['WEP']}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td className='clickable' onClick={onSysClicked}>{translate('SYS')}</td>
<td className='clickable' onClick={onRstClicked}>{translate('RST')}</td>
<td className='clickable' onClick={onWepClicked}>{translate('WEP')}</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -17,7 +17,7 @@ const POWER = [
* Power Management Section * Power Management Section
*/ */
export default class PowerManagement extends TranslatedComponent { export default class PowerManagement extends TranslatedComponent {
static PropTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired, ship: React.PropTypes.object.isRequired,
code: React.PropTypes.string.isRequired, code: React.PropTypes.string.isRequired,
onChange: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired

View File

@@ -0,0 +1,126 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import Ship from '../shipyard/Ship';
import { Ships } from 'coriolis-data/dist';
import { Rocket } from './SvgIcons';
import Persist from '../stores/Persist';
import cn from 'classnames';
/**
* Ship picker
* Requires an onChange() function of the form onChange(ship), providing the ship, which is triggered on ship change
*/
export default class ShipPicker extends TranslatedComponent {
static propTypes = {
onChange: React.PropTypes.func.isRequired,
ship: React.PropTypes.string.isRequired,
build: React.PropTypes.string
};
static defaultProps = {
ship: 'eagle'
}
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this.shipOrder = Object.keys(Ships).sort();
this._toggleMenu = this._toggleMenu.bind(this);
this._closeMenu = this._closeMenu.bind(this);
this.state = { menuOpen: false };
}
/**
* Update ship
* @param {object} ship the ship
* @param {string} build the build, if present
*/
_shipChange(ship, build) {
this._closeMenu();
// Ensure that the ship has changed
if (ship !== this.props.ship || build !== this.props.build) {
this.props.onChange(ship, build);
}
}
/**
* Render the menu for the picker
* @returns {object} the picker menu
*/
_renderPickerMenu() {
const { ship, build } = this.props;
const _shipChange = this._shipChange;
const builds = Persist.getBuilds();
const buildList = [];
for (let shipId of this.shipOrder) {
const shipBuilds = [];
// Add stock build
const stockSelected = (ship == shipId && !build);
shipBuilds.push(<li key={shipId} className={ cn({ 'selected': stockSelected })} onClick={_shipChange.bind(this, shipId, null)}>Stock</li>);
if (builds[shipId]) {
let buildNameOrder = Object.keys(builds[shipId]).sort();
for (let buildName of buildNameOrder) {
const buildSelected = ship === shipId && build === buildName;
shipBuilds.push(<li key={shipId + '-' + buildName} className={ cn({ 'selected': buildSelected })} onClick={_shipChange.bind(this, shipId, buildName)}>{buildName}</li>);
}
}
buildList.push(<ul key={shipId} className='block'>{Ships[shipId].properties.name}{shipBuilds}</ul>);
}
return buildList;
}
/**
* Toggle the menu state
*/
_toggleMenu() {
const { menuOpen } = this.state;
this.setState({ menuOpen: !menuOpen });
}
/**
* Close the menu
*/
_closeMenu() {
const { menuOpen } = this.state;
if (menuOpen) {
this._toggleMenu();
}
}
/**
* Render picker
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { ship, build } = this.props;
const { menuOpen } = this.state;
const shipString = ship + ': ' + (build ? build : translate('stock'));
return (
<div className='shippicker' onClick={ (e) => e.stopPropagation() }>
<div className='menu'>
<div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}>
<span><Rocket className='warning' /></span>
<span className='menu-item-label'>{shipString}</span>
</div>
{ menuOpen ?
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
<div className='quad'>
{this._renderPickerMenu()}
</div>
</div> : null }
</div>
</div>
);
}
}

View File

@@ -8,7 +8,7 @@ import { Rocket } from './SvgIcons';
* Selector for ships * Selector for ships
*/ */
export default class ShipSelector extends TranslatedComponent { export default class ShipSelector extends TranslatedComponent {
static PropTypes = { static propTypes = {
initial: React.PropTypes.object.isRequired, initial: React.PropTypes.object.isRequired,
onChange: React.PropTypes.func.isRequired onChange: React.PropTypes.func.isRequired
}; };

View File

@@ -2,6 +2,7 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames'; import cn from 'classnames';
import { Warning } from './SvgIcons'; import { Warning } from './SvgIcons';
import * as Calc from '../shipyard/Calculations';
/** /**
* Ship Summary Table / Stats * Ship Summary Table / Stats
@@ -9,7 +10,8 @@ import { Warning } from './SvgIcons';
export default class ShipSummaryTable extends TranslatedComponent { export default class ShipSummaryTable extends TranslatedComponent {
static propTypes = { static propTypes = {
ship: React.PropTypes.object.isRequired ship: React.PropTypes.object.isRequired,
marker: React.PropTypes.string.isRequired,
}; };
/** /**
@@ -17,77 +19,75 @@ export default class ShipSummaryTable extends TranslatedComponent {
* @return {React.Component} Summary table * @return {React.Component} Summary table
*/ */
render() { render() {
let ship = this.props.ship; const { ship } = this.props;
let { language, tooltip, termtip } = this.context; let { language, tooltip, termtip } = this.context;
let translate = language.translate; let translate = language.translate;
let u = language.units; let u = language.units;
let formats = language.formats; let formats = language.formats;
let { time, int, round, f1, f2 } = formats; let { time, int, round, f1, f2 } = formats;
let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') });
let sgRecover = '-';
let sgRecharge = '-';
let hide = tooltip.bind(null, null); let hide = tooltip.bind(null, null);
if (ship.shield) { const shieldGenerator = ship.findInternalByGroup('sg');
sgRecover = time(ship.calcShieldRecovery()); const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator });
sgRecharge = time(ship.calcShieldRecharge()); const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
} const timeToDrain = Calc.timeToDrainWep(ship, 4);
const canThrust = ship.canThrust();
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
const canBoost = ship.canBoost();
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
return <div id='summary'> return <div id='summary'>
<table id='summaryTable'> <table id='summaryTable'>
<thead> <thead>
<tr className='main'> <tr className='main'>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canThrust() }) }>{translate('speed')}</th> <th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !ship.canBoost() }) }>{translate('boost')}</th> <th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
<th onMouseEnter={termtip.bind(null, 'damage per second')} onMouseLeave={hide} rowSpan={2}>{translate('DPS')}</th> <th colSpan={5}>{translate('jump range')}</th>
<th onMouseEnter={termtip.bind(null, 'energy per second')} onMouseLeave={hide} rowSpan={2}>{translate('EPS')}</th> <th rowSpan={2}>{translate('shield')}</th>
<th onMouseEnter={termtip.bind(null, 'time to drain WEP capacitor')} onMouseLeave={hide} rowSpan={2}>{translate('TTD')}</th> <th rowSpan={2}>{translate('integrity')}</th>
<th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th> <th rowSpan={2}>{translate('DPS')}</th>
<th onMouseEnter={termtip.bind(null, 'hull hardness')} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th> <th rowSpan={2}>{translate('EPS')}</th>
<th onMouseEnter={termtip.bind(null, 'armour')} onMouseLeave={hide} rowSpan={2}>{translate('arm')}</th> <th rowSpan={2}>{translate('TTD')}</th>
<th onMouseEnter={termtip.bind(null, 'shields')} onMouseLeave={hide} rowSpan={2}>{translate('shld')}</th> {/* <th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th> */}
<th colSpan={3}>{translate('mass')}</th>
<th rowSpan={2}>{translate('cargo')}</th> <th rowSpan={2}>{translate('cargo')}</th>
<th rowSpan={2}>{translate('fuel')}</th> <th rowSpan={2}>{translate('fuel')}</th>
<th colSpan={3}>{translate('jump range')}</th> <th colSpan={3}>{translate('mass')}</th>
<th onMouseEnter={termtip.bind(null, 'PHRASE_FASTEST_RANGE')} onMouseLeave={hide} colSpan={3}>{translate('fastest range')}</th> <th onMouseEnter={termtip.bind(null, 'hull hardness', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('hrd')}</th>
<th rowSpan={2}>{translate('crew')}</th> <th rowSpan={2}>{translate('crew')}</th>
<th onMouseEnter={termtip.bind(null, 'mass lock factor')} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th> <th onMouseEnter={termtip.bind(null, 'mass lock factor', { cap: 0 })} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
</tr> </tr>
<tr> <tr>
<th className='lft'>{translate('hull')}</th>
<th onMouseEnter={termtip.bind(null, 'PHRASE_UNLADEN', { cap: 0 })} onMouseLeave={hide}>{translate('unladen')}</th>
<th onMouseEnter={termtip.bind(null, 'PHRASE_LADEN', { cap: 0 })} onMouseLeave={hide}>{translate('laden')}</th>
<th className='lft'>{translate('max')}</th> <th className='lft'>{translate('max')}</th>
<th>{translate('full tank')}</th> <th>{translate('unladen')}</th>
<th>{translate('laden')}</th> <th>{translate('laden')}</th>
<th className='lft'>{translate('jumps')}</th> <th>{translate('total unladen')}</th>
<th>{translate('total laden')}</th>
<th className='lft'>{translate('hull')}</th>
<th>{translate('unladen')}</th> <th>{translate('unladen')}</th>
<th>{translate('laden')}</th> <th>{translate('laden')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{ ship.canThrust() ? <span>{int(ship.topSpeed)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td> <td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })} onMouseLeave={hide}>{ canThrust ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{ ship.canBoost() ? <span>{int(ship.topBoost)} {u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td> <td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })} onMouseLeave={hide}>{ canBoost ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td>{f1(ship.totalDps)}</td> <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump()))}{u.LY}</span></td>
<td>{f1(ship.totalEps)}</td> <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
<td>{ship.timeToDrain === Infinity ? '∞' : time(ship.timeToDrain)}</td> <td><span onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</span></td>
<td>{f1(ship.totalHps)}</td> <td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity))}{u.LY}</td>
<td className={sgClassNames} onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })} onMouseLeave={hide}>{int(ship.shield)}{u.MJ}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })} onMouseLeave={hide}>{int(ship.armour)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalDps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalEps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })} onMouseLeave={hide}>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
{/* <td>{f1(ship.totalHps)}</td> */}
<td>{round(ship.cargoCapacity)}{u.T}</td>
<td>{round(ship.fuelCapacity)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })} onMouseLeave={hide}>{ship.hullMass}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.unladenMass)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.ladenMass)}{u.T}</td>
<td>{int(ship.hardness)}</td> <td>{int(ship.hardness)}</td>
<td>{int(ship.armour)}</td>
<td className={sgClassNames}>{int(ship.shield)} {u.MJ}</td>
<td>{ship.hullMass} {u.T}</td>
<td>{int(ship.unladenMass)} {u.T}</td>
<td>{int(ship.ladenMass)} {u.T}</td>
<td>{round(ship.cargoCapacity)} {u.T}</td>
<td>{round(ship.fuelCapacity)} {u.T}</td>
<td>{f2(ship.unladenRange)} {u.LY}</td>
<td>{f2(ship.fullTankRange)} {u.LY}</td>
<td>{f2(ship.ladenRange)} {u.LY}</td>
<td>{int(ship.maxJumpCount)}</td>
<td>{f2(ship.unladenFastestRange)} {u.LY}</td>
<td>{f2(ship.ladenFastestRange)} {u.LY}</td>
<td>{ship.crew}</td> <td>{ship.crew}</td>
<td>{ship.masslock}</td> <td>{ship.masslock}</td>
</tr> </tr>

View File

@@ -8,6 +8,7 @@ import ModificationsMenu from './ModificationsMenu';
import { ListModifications, Modified } from './SvgIcons'; import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions'; import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/** /**
* Standard Slot * Standard Slot
@@ -53,6 +54,12 @@ export default class StandardSlot extends TranslatedComponent {
let modTT = translate('modified'); let modTT = translate('modified');
if (m && m.blueprint && m.blueprint.name) { if (m && m.blueprint && m.blueprint.name) {
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade; modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade].features, m)}
</div>
);
} }
if (!selected) { if (!selected) {
@@ -93,7 +100,7 @@ export default class StandardSlot extends TranslatedComponent {
{ m.getMinMass() ? <div className='l'>{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}</div> : null } { m.getMinMass() ? <div className='l'>{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}</div> : null }
{ m.getOptMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}</div> : null } { m.getOptMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}</div> : null }
{ m.getMaxMass() ? <div className='l'>{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}</div> : null } { m.getMaxMass() ? <div className='l'>{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}</div> : null }
{ m.getRange() ? <div className='l'>{translate('range')}: {formats.f2(m.getRange())}{units.km}</div> : null } { m.getRange() ? <div className='l'>{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}</div> : null }
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null } { m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.getThermalEfficiency() ? <div className='l'>{translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}</div> : null } { m.getThermalEfficiency() ? <div className='l'>{translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}</div> : null }
{ m.getPowerGeneration() > 0 ? <div className='l'>{translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}</div> : null } { m.getPowerGeneration() > 0 ? <div className='l'>{translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}</div> : null }

View File

@@ -708,6 +708,24 @@ export class Switch extends SvgIcon {
} }
} }
/**
* Pip
*/
export class Pip extends SvgIcon {
/**
* Overriden view box
* @return {String} view box
*/
viewBox() { return '0 0 200 200'; }
/**
* Generate the SVG
* @return {React.Component} SVG Contents
*/
svg() {
return <rect x='10' y='10' width='180' height='180'/>;
}
}
/** /**
* In-game Coriolis Station logo * In-game Coriolis Station logo
*/ */

View File

@@ -0,0 +1,105 @@
import TranslatedComponent from './TranslatedComponent';
import React, { PropTypes } from 'react';
import Measure from 'react-measure';
import { BarChart, Bar, XAxis, YAxis } from 'recharts';
const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D'];
const LABEL_COLOUR = '#000000';
const AXIS_COLOUR = '#C06400';
const ASPECT = 1;
const merge = function(one, two) {
return Object.assign({}, one, two);
};
/**
* A vertical bar chart
*/
export default class VerticalBarChart extends TranslatedComponent {
static propTypes = {
data : PropTypes.array.isRequired,
yMax : PropTypes.number
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this._termtip = this._termtip.bind(this);
this.state = {
dimensions: {
width: 300,
height: 300
}
};
}
/**
* Render the bar chart
* @returns {Object} the markup
*/
render() {
const { width, height } = this.state.dimensions;
const { tooltip, termtip } = this.context;
// Calculate maximum for Y
let dataMax = Math.max(...this.props.data.map(d => d.value));
if (dataMax == -Infinity) dataMax = 0;
let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0;
const localMax = Math.max(dataMax, yMax);
return (
<Measure whitelist={['width', 'top']} onMeasure={ (dimensions) => this.setState({ dimensions }) }>
<div width='100%'>
<BarChart width={width} height={width * ASPECT} data={this.props.data} margin={{ top: 5, right: 5, left: 5, bottom: 5 }}>
<XAxis interval={0} fontSize='0.8em' stroke={AXIS_COLOUR} dataKey='label' />
<YAxis interval={'preserveStart'} tickCount={11} fontSize='0.8em' stroke={AXIS_COLOUR} type='number' domain={[0, localMax]}/>
<Bar dataKey='value' label={<ValueLabel />} fill={CORIOLIS_COLOURS[0]} isAnimationActive={false} onMouseOver={this._termtip} onMouseOut={tooltip.bind(null, null)}/>
</BarChart>
</div>
</Measure>
);
}
/**
* Generate a term tip
* @param {Object} d the data
* @param {number} i the index
* @param {Object} e the event
* @returns {Object} termtip markup
*/
_termtip(d, i, e) {
if (this.props.data[i].tooltip) {
return this.context.termtip(this.props.data[i].tooltip, e);
} else {
return null;
}
}
}
/**
* A label that displays the value within the bar of the chart
*/
const ValueLabel = React.createClass({
propTypes: {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
value: PropTypes.number
},
render() {
const { x, y, payload, value } = this.props;
return (
<text x={x} y={y} fill="#000000" textAnchor="middle" dy={20}>{value}</text>
);
}
});

View File

@@ -0,0 +1,204 @@
import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import ShipSelector from './ShipSelector';
import { nameComparator } from '../utils/SlotFunctions';
import { CollapseSection, ExpandSection, MountFixed, MountGimballed, MountTurret } from './SvgIcons';
import LineChart from '../components/LineChart';
import Slider from '../components/Slider';
import * as Calc from '../shipyard/Calculations';
import Module from '../shipyard/Module';
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
/**
* Weapon damage chart
*/
export default class WeaponDamageChart extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
opponent: React.PropTypes.object.isRequired,
hull: React.PropTypes.bool.isRequired,
engagementRange: React.PropTypes.number.isRequired,
opponentSys: React.PropTypes.number.isRequired,
marker: React.PropTypes.string.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
}
/**
* Set the initial weapons state
*/
componentWillMount() {
const weaponNames = this._weaponNames(this.props.ship, this.context);
const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys);
const opponentArmour = Calc.armourMetrics(this.props.opponent);
const maxRange = this._calcMaxRange(this.props.ship);
const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour);
this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) });
}
/**
* Set the updated weapons state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.marker != this.props.marker) {
const weaponNames = this._weaponNames(nextProps.ship, nextContext);
const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys);
const opponentArmour = Calc.armourMetrics(nextProps.opponent);
const maxRange = this._calcMaxRange(nextProps.ship);
const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour);
this.setState({ weaponNames,
opponentShields,
opponentArmour,
maxRange,
maxDps,
calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull)
});
}
return true;
}
/**
* Calculate the maximum range of a ship's weapons
* @param {Object} ship The ship
* @returns {int} The maximum range, in metres
*/
_calcMaxRange(ship) {
let maxRange = 1000; // Minimum
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const thisRange = ship.hardpoints[i].m.getRange();
if (thisRange > maxRange) {
maxRange = thisRange;
}
}
}
return maxRange;
}
/**
* Calculate the maximum sustained single-weapon DPS for this ship
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {Object} opponentShields The opponent's shields
* @param {Object} opponentArmour The opponent's armour
* @return {number} The maximum sustained single-weapon DPS
*/
_calcMaxSDps(ship, opponent, opponentShields, opponentArmour) {
// Additional information to allow effectiveness calculations
let maxSDps = 0;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0);
const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
if (thisSDps > maxSDps) {
maxSDps = thisSDps;
}
}
}
return maxSDps;
}
/**
* Obtain the weapon names for this ship
* @param {Object} ship The ship
* @param {Object} context The context
* @return {array} The weapon names
*/
_weaponNames(ship, context) {
const translate = context.language.translate;
let names = [];
let num = 1;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp);
let engineering;
if (m.blueprint && m.blueprint.name) {
engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id) {
engineering += ', ' + translate(m.blueprint.special.name);
}
}
if (engineering) {
name = name + ' (' + engineering + ')';
}
names.push(name);
}
}
return names;
}
/**
* Calculate the per-weapon sustained DPS for this ship against another ship at a given range
* @param {Object} ship The ship
* @param {Object} weaponNames The names of the weapons for which to calculate DPS
* @param {Object} opponent The target
* @param {Object} opponentShields The opponent's shields
* @param {Object} opponentArmour The opponent's armour
* @param {bool} hull true if to calculate against hull, false if to calculate against shields
* @param {Object} engagementRange The engagement range
* @return {array} The array of weapon DPS
*/
_calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) {
let results = {};
let weaponNum = 0;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange);
results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
}
}
return results;
}
/**
* Render damage dealt
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { maxRange } = this.state;
const { ship, opponent, engagementRange } = this.props;
const sortOrder = this._sortOrder;
const onCollapseExpand = this._onCollapseExpand;
const code = `${ship.toString()}:${opponent.toString()}`;
return (
<span>
<LineChart
xMax={maxRange}
yMax={this.state.maxDps}
xLabel={translate('range')}
xUnit={translate('m')}
yLabel={translate('sdps')}
series={this.state.weaponNames}
xMark={this.props.engagementRange}
colors={DAMAGE_DEALT_COLORS}
func={this.state.calcSDpsFunc}
points={200}
code={code}
/>
</span>
);
}
}

View File

@@ -58,21 +58,21 @@ export function getLanguage(langCode) {
}, },
translate, translate,
units: { units: {
CR: <u> {translate('CR')}</u>, // Credits CR: <u>{translate('CR')}</u>, // Credits
kg: <u> {translate('kg')}</u>, // Kilograms kg: <u>{translate('kg')}</u>, // Kilograms
kgs: <u> {translate('kg/s')}</u>, // Kilograms per second kgs: <u>{translate('kg/s')}</u>, // Kilograms per second
km: <u> {translate('km')}</u>, // Kilometers km: <u>{translate('km')}</u>, // Kilometers
Ls: <u> {translate('Ls')}</u>, // Light Seconds Ls: <u>{translate('Ls')}</u>, // Light Seconds
LY: <u> {translate('LY')}</u>, // Light Years LY: <u>{translate('LY')}</u>, // Light Years
MJ: <u> {translate('MJ')}</u>, // Mega Joules MJ: <u>{translate('MJ')}</u>, // Mega Joules
'm/s': <u> {translate('m/s')}</u>, // Meters per second 'm/s': <u>{translate('m/s')}</u>, // Meters per second
'°/s': <u> {translate('°/s')}</u>, // Degrees per second '°/s': <u>{translate('°/s')}</u>, // Degrees per second
MW: <u> {translate('MW')}</u>, // Mega Watts (same as Mega Joules per second) MW: <u>{translate('MW')}</u>, // Mega Watts (same as Mega Joules per second)
mps: <u>{translate('m/s')}</u>, // Metres per second mps: <u>{translate('m/s')}</u>, // Metres per second
ps: <u>{translate('/s')}</u>, // per second ps: <u>{translate('/s')}</u>, // per second
pm: <u>{translate('/min')}</u>, // per minute pm: <u>{translate('/min')}</u>, // per minute
s: <u>{translate('secs')}</u>, // Seconds s: <u>{translate('secs')}</u>, // Seconds
T: <u> {translate('T')}</u>, // Metric Tons T: <u>{translate('T')}</u>, // Metric Tons
} }
}; };
} }

View File

@@ -24,8 +24,8 @@ export const terms = {
PHRASE_NO_BUILDS: 'No builds added to comparison!', PHRASE_NO_BUILDS: 'No builds added to comparison!',
PHRASE_NO_RETROCH: 'No Retrofitting changes', PHRASE_NO_RETROCH: 'No Retrofitting changes',
PHRASE_SELECT_BUILDS: 'Select builds to compare', PHRASE_SELECT_BUILDS: 'Select builds to compare',
PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge', PHRASE_SG_RECHARGE: 'Time from 50% to 100% charge, assuming full SYS capacitor to start with',
PHRASE_SG_RECOVER: 'Recovery (to 50%) after collapse', PHRASE_SG_RECOVER: 'Time from 0% to 50% charge, assuming full SYS capacitor to start with',
PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo',
PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh',
PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target', PHRASE_ENGAGEMENT_RANGE: 'The distance between your ship and its target',
@@ -39,6 +39,55 @@ export const terms = {
PHRASE_NO_SPECIAL: 'No experimental effect', PHRASE_NO_SPECIAL: 'No experimental effect',
PHRASE_SHOPPING_LIST: 'Stations that sell this build', PHRASE_SHOPPING_LIST: 'Stations that sell this build',
PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells', PHRASE_TOTAL_EFFECTIVE_SHIELD: 'Total amount of damage that can be taken from each damage type, if using all shield cells',
PHRASE_TIME_TO_LOSE_SHIELDS: 'Shields will hold for',
PHRASE_TIME_TO_RECOVER_SHIELDS: 'Shields will recover in',
PHRASE_TIME_TO_RECHARGE_SHIELDS: 'Shields will recharge in',
PHRASE_SHIELD_SOURCES: 'Breakdown of the supply of shield energy',
PHRASE_EFFECTIVE_SHIELD: 'Effective shield strength against different damage types',
PHRASE_ARMOUR_SOURCES: 'Breakdown of the supply of armour',
PHRASE_EFFECTIVE_ARMOUR: 'Effective armour strength against different damage types',
PHRASE_DAMAGE_TAKEN: '% of raw damage taken for different damage types',
PHRASE_TIME_TO_LOSE_ARMOUR: 'Armour will hold for',
PHRASE_MODULE_PROTECTION_EXTERNAL: 'Protection for hardpoints',
PHRASE_MODULE_PROTECTION_INTERNAL: 'Protection for all other modules',
PHRASE_SHIELD_DAMAGE: 'Breakdown of sources for sustained DPS against shields',
PHRASE_ARMOUR_DAMAGE: 'Breakdown of sources for sustained DPS against armour',
PHRASE_TIME_TO_REMOVE_SHIELDS: 'Will remove shields in',
TT_TIME_TO_REMOVE_SHIELDS: 'With sustained fire by all weapons',
PHRASE_TIME_TO_REMOVE_ARMOUR: 'Will remove armour in',
TT_TIME_TO_REMOVE_ARMOUR: 'With sustained fire by all weapons',
PHRASE_TIME_TO_DRAIN_WEP: 'Will drain WEP in',
TT_TIME_TO_DRAIN_WEP: 'Time to drain WEP capacitor with all weapons firing',
TT_TIME_TO_LOSE_SHIELDS: 'Against sustained fire from all opponent\'s weapons',
TT_TIME_TO_LOSE_ARMOUR: 'Against sustained fire from all opponent\'s weapons',
TT_MODULE_ARMOUR: 'Armour protecting against module damage',
TT_MODULE_PROTECTION_EXTERNAL: 'Percentage of damage diverted from hardpoints to module reinforcement packages',
TT_MODULE_PROTECTION_INTERNAL: 'Percentage of damage diverted from non-hardpoint modules to module reinforcement packages',
TT_EFFECTIVE_SDPS_SHIELDS: 'Actual sustained DPS whilst WEP capacitor is not empty',
TT_EFFECTIVENESS_SHIELDS: 'Effectivness compared to hitting a 0-resistance target with 0 pips to SYS at 0m',
TT_EFFECTIVE_SDPS_ARMOUR: 'Actual sustained DPS whilst WEP capacitor is not empty',
TT_EFFECTIVENESS_ARMOUR: 'Effectivness compared to hitting a 0-resistance target at 0m',
TT_SUMMARY_SPEED: 'With full fuel tank and 4 pips to ENG',
TT_SUMMARY_SPEED_NONFUNCTIONAL: 'Thrusters powered off or over maximum mass',
TT_SUMMARY_BOOST: 'With full fuel tank and 4 pips to ENG',
TT_SUMMARY_BOOST_NONFUNCTIONAL: 'Power distributor not able to supply enough power to boost',
TT_SUMMARY_SHIELDS: 'Raw shield strength, including boosters',
TT_SUMMARY_SHIELDS_NONFUNCTIONAL: 'No shield generator or shield generator powered off',
TT_SUMMARY_INTEGRITY: 'Ship integrity, including bulkheads and hull reinforcement packages',
TT_SUMMARY_HULL_MASS: 'Mass of the hull prior to any modules being installed',
TT_SUMMARY_UNLADEN_MASS: 'Mass of the hull and modules prior to any fuel or cargo',
TT_SUMMARY_LADEN_MASS: 'Mass of the hull and modules with full fuel and cargo',
TT_SUMMARY_DPS: 'Damage per second with all weapons firing',
TT_SUMMARY_EPS: 'WEP capacitor consumed per second with all weapons firing',
TT_SUMMARY_TTD: 'Time to drain WEP capacitor with all weapons firing and 4 pips to WEP',
TT_SUMMARY_MAX_SINGLE_JUMP: 'Farthest possible jump range with no cargo and only enough fuel for the jump itself',
TT_SUMMARY_UNLADEN_SINGLE_JUMP: 'Farthest possible jump range with no cargo and a full fuel tank',
TT_SUMMARY_LADEN_SINGLE_JUMP: 'Farthest possible jump range with full cargo and a full fuel tank',
TT_SUMMARY_UNLADEN_TOTAL_JUMP: 'Farthest possible range with no cargo, a full fuel tank, and jumping as far as possible each time',
TT_SUMMARY_LADEN_TOTAL_JUMP: 'Farthest possible range with full cargo, a full fuel tank, and jumping as far as possible each time',
HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes',
@@ -181,8 +230,7 @@ export const terms = {
regen: 'Regeneration rate', regen: 'Regeneration rate',
reload: 'Reload', reload: 'Reload',
rof: 'Rate of fire', rof: 'Rate of fire',
scanangle: 'Scan angle', angle: 'Scan angle',
scanrange: 'Scan range',
scantime: 'Scan time', scantime: 'Scan time',
shield: 'Shield', shield: 'Shield',
shieldboost: 'Shield boost', shieldboost: 'Shield boost',
@@ -204,6 +252,23 @@ export const terms = {
optmul_sg: 'Optimal strength', optmul_sg: 'Optimal strength',
maxmul_sg: 'Minimum strength', maxmul_sg: 'Minimum strength',
range_s: 'Typical emission range',
// Damage types
absolute: 'Absolute',
explosive: 'Explosive',
kinetic: 'Kinetic',
thermal: 'Thermal',
// Shield sources
generator: 'Generator',
boosters: 'Boosters',
cells: 'Cells',
// Armour sources
bulkheads: 'Bulkheads',
reinforcement: 'Reinforcement',
// Help text // Help text
HELP_TEXT: ` HELP_TEXT: `
<h1>Introduction</h1> <h1>Introduction</h1>
@@ -234,7 +299,7 @@ Along the top of the screen are some of the key values for your build. This is
Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.</p> Here, along with most places in Coriolis, acronyms will have tooltips explaining what they mean. Hover over the acronym to obtain more detail, or look in the glossary at the end of this help.</p>
All values are the highest possible, assuming that you have maximum pips in the relevant capacitor (ENG for speed, WEP for time to drain, etc.).</p> All values are the highest possible, assuming that you an optimal setup for that particular value (maximum pips in ENG for speed, minimum fuel for jump range, etc.). Details of the specific setup for each value are listed in the associated tootip.</p>
<h2>Modules</h2> <h2>Modules</h2>
The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions). </p> The next set of panels laid out horizontally across the screen contain the modules you have put in your build. From left to right these are the core modules, the internal modules, the hardpoints and the utility mounts. These represent the available slots in your ship and cannot be altered. Each slot has a class, or size, and in general any module up to a given size can fit in a given slot (exceptions being bulkheads, life support and sensors in core modules and restricted internal slots, which can only take a subset of module depending on their restrictions). </p>
@@ -247,30 +312,39 @@ To move a module from one slot to another drag it. If you instead want to copy
Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers). </p> Clicking on the headings for each set of modules gives you the ability to either select an overall role for your ship (when clicking the core internal header) or a specific module with which you want to fill all applicable slots (when clicking the other headers). </p>
<h2>Offence Summary</h2> <h2>Ship Controls</h2>
The offence summary panel provides information about the damage that you deal with your weapons.</p> The ship controls allow you to set your pips, boost, and amount of fuel and cargo that your build carries. The changes made here will effect the information supplied in the subsequent panels, giving you a clearer view of what effect different changing these items will have. </p>
The first headline gives an overall damage per second rating: this is the optimum amount of damage the build will do per second according to weapon statistics. After that is a breakdown of the damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.</p> Ship control settings are saved as part of a build. </p>
The next headline gives an overall sustained damage per second rating: this is the optimum amount of damage the build will do per second over a longer period of time, taking in to account ammunition clip capacities and reload times. After that is a breakdown of the sustained damage per second the build will do for each type of damage: absolute, explosive, kinetic, and thermal.</p> <h2>Opponent</h2>
The opponet selection allows you to choose your opponent. The opponent can be either a stock build of a ship or one of your own saved builds. You can also set the engagement range between you and your opponent. Your selection here will effect the information supplied in the subsequent panels, specifically the Offence and Defence panels. </p>
The final headline gives an overall damage per energy rating: this is the amount of damage the build will do per unit of weapon capacitor energy expended. After that is a breakdown of the damage per energy the build will do for each type of damage: absolute, explosive, kinetic, and thermal.</p> Opponent settings are saved as part of a build. </p>
<h2>Defence Summary</h2> <h2>Power and Costs Sub-panels</h2>
The defence summary panel provides information about the strength of your defences and the damage that you receive from opponents.</p> <h3>Power</h3>
The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module. Disabled modules will not be included in the build&apos;s statistics, with the exception of Shield Cell Banks as they are usually disabled when not in use and only enabled when required. </p>
The first headline gives your total shield strength (if you have shields), taking in to account your base shield plus boosters. After that are the details of how long it will take for your shields to recover from 0 to 50% (recovery time) and from 50% to 100% (recharge time). The next line provides a breakdown of the shield damage taken from different damage types. For example, if you damage from kinetic is 60% then it means that a weapon usually dealing 10 points of damage will only deal 6, the rest being resisted by the shield. Note that this does not include any resistance alterations due to pips in your SYS capacitor.</p> <h3>Costs</h3>
The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the &apos;Settings&apos; at the top-right of the page.</p>
The second headline gives your total shield cell strength (if you have shield cell banks). This is the sum of the recharge of all of equipped shield cell banks.</p> The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.</p>
The third headline gives your total armour strength, taking in to account your base armour plus hull reinforcement packages. The next line provides a breakdown of the hull damage taken from different damage types. For example, if you damage from kinetic is 120% then it means that a weapon usually dealing 10 points of damage will deal 12.</p> The reload costs provides information about the costs of reloading your current build.</p>
The fourth headline gives your total module protection strength from module reinforcement packages. The next line provides a breakdown of the protection for both internal and external modules whilst all module reinforcement packages are functioning. For example, if external module protection is 20% then 10 points of damage will 2 points of damage to the module reinforcement packages and 8 points of damage to the module</p> <h2>Profiles</h2>
Profiles provide graphs that show the general performance of modules in your build
<h2>Movement Summary</h2> <h3>Engine Profile</h3>
The movement summary panel provides information about the build&apos;s speed and agility.</p> The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed alters with the overall mass of your build. The vertical dashed line on the graph shows your current mass. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your maximum speed by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your speed by hitting the boost button. </p>
<h3>FSD Profile</h3>
The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range alters with the overall mass of your build. The vertical dashed line on the graph shows your current maximum single jump range. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD, and you can increase your maximum jump range by reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build, </p>
<h3>Movement Profile</h3>
The movement profile panel provides information about the capabilities of your current thrusters with your current overall mass and ENG pips settings. The diagram shows your ability to move and rotate in the different axes:
Along the top of this panel are the number of pips you put in to your ENG capacitor, from 0 to 4 and also include 4 pips and boost (4b). Along the side of this panel are the names of the metrics. These are:
<dl> <dl>
<dt>Speed</dt><dd>The fastest the ship can move, in metres per second</dd> <dt>Speed</dt><dd>The fastest the ship can move, in metres per second</dd>
<dt>Pitch</dt><dd>The fastest the ship can raise or lower its nose, in degrees per second</dd> <dt>Pitch</dt><dd>The fastest the ship can raise or lower its nose, in degrees per second</dd>
@@ -278,51 +352,84 @@ Along the top of this panel are the number of pips you put in to your ENG capaci
<dt>Yaw</dt><dd>The fastest the ship can turn its nose left or right, in degrees per second</dd> <dt>Yaw</dt><dd>The fastest the ship can turn its nose left or right, in degrees per second</dd>
</dl> </dl>
<h2>Power Management</h2> Your movement profile can be altered by obtaining different thrusters or engineering your existing thrusters, and you can increase your movement values by adding pips to the ENG capacitor as well as reducing the amount of fuel and cargo you are carrying as well as reducing the overall weight of the build. You can also temporarily increase your movement profile by hitting the boost button. </p>
The power management panel provides information about power usage and priorities. It allows you to enable and disable individual modules, as well as set power priorities for each module.</p>
<h2>Costs</h2> <h3>Damage Profile</h3>
The costs panel provides information about the costs for each of your modules, and the total cost and insurance for your build. By default Coriolis uses the standard costs, however discounts for your ship, modules and insurance can be altered in the &apos;Settings&apos; at the top-right of the page.</p> The damage profile provides two graphs showing how the the build&apos;s damage to the opponent&apos;s shields and hull change with engagement range. The vertical dashed line on the graph shows your current engagement range. This combines information about the build&apos;s weapons with the opponent&apos;s shields and hull to provide an accurate picture of sustained damage that can be inflicted on the opponent. </p>
The retrofit costs provides information about the costs of changing the base build for your ship, or your saved build, to the current build.</p> <h2>Offence</h2>
<h3>Summary</h3>
The offence summary provides per-weapon information about sustained damage per second inflicted to shields and hull, along with a measure of effectiveness of that weapon. The effectiveness value has a tooltip that provides a breakdown of the effectiveness, and can include reductions or increases due to range, resistance, and either power distributor (for shields) or hardness (for hull). The final effectiveness value is calculated by multiplying these percentages together. </p>
The reload costs provides information about the costs of reloading your current build.</p> <h3>Offence Metrics</h3>
The offence metrics panel provides information about your offence. </p>
<h2>Engine Profile</h2> Time to drain is a measure of how quickly your WEP capacitor will drain when firing all weapons. It is affected by the number of pips you have in your WEP capacitor, with more pips resulting in a higher WEP recharge rate and hence a longer time to drain. </p>
The engine profile panel provides information about the capabilities of your current thrusters. The graph shows you how the maximum speed (with a full fuel tank and 4 pips to engines) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your engine profile can be altered by obtaining different thrusters or engineering your existing thrusters.</p>
<h2>FSD Profile</h2> The next value is the time it will take you to remove your opponent&apos;s shields. This assumes that you have 100% time on target and that your engagement range stays constant. Note that if your time to remove shields is longer than your time to drain this assumes that you continue firing throughout, inflicting lower damage due to the reduced energy in your WEP capacitor. </p>
The FSD profile panel provides information about the capabilities of your current frame shift drive. The graph shows you how the maximum jump range (with a full fuel tank) alters with the overall mass of your build. The slider can be altered to change the amount of cargo you have on-board. Your FSD profile can be altered by obtaining a different FSD or engineering your existing FSD.</p>
<h2>Jump Range</h2> The next value is the time it will take you to remove your opponent&apos;s armour. This follows the same logic as the time to remove shields. </p>
The jump range panel provides information about the build&apos; jump range. The graph shows how the build&apos;s jump range changes with the amount of cargo on-board. The slider can be altered to change the amount of fuel you have on-board.</p>
<h2>Damage Dealt</h2> <h3>Shield Damage Sources</h3>
The damage dealt panel provides information about the effectiveness of your build&apos;s weapons against opponents&apos; shields and hull at different engagement distances.</p> The shield damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided. </p>
The ship against which you want to check damage dealt can be selected by clicking on the red ship icon or the red ship name at the top of this panel.</p> <h3>Hull Damage Sources</h3>
The hull damage sources provides information about the sources of damage to your opponent by damage type. For each applicable type of damage (absolute explosive, kinetic, thermal) a sustained damage per second value is provided. </p>
The main section of this panel is a table showing your weapons and their effectiveness. Effectiveness against shields takes in to account the weapon and its engagement range, and assumes standard shield resistances. Effectiveness against hull takes in to account the weapon and, its engagement range and the target&apos;s hardness, and assumes military grade armour resistances.</p> <h2>Defence</h2>
<h3>Shield Metrics</h3>
The shield metrics provides information about your shield defence. </p>
Effective DPS and effective SDPS are the equivalent of DPS and SDPS for the weapon. Effectiveness is a percentage value that shows how effective the DPS of the weapon is compared in reality against the given target compared to the weapon&apos;s stated DPS. Effectiveness can never go above 100%.</p> Raw shield strength is the sum of the shield from your generator, boosters and shield cell banks. A tooltip provides a breakdown of these values. </p>
Total effective DPS, SDPS and effectiveness against both shields and hull are provided at the bottom of the table.</p> The time the shields will hold for is the time it will take your opponent&apos; to remove your shields. This assumes that they have 100% time on target and that the engagement range stays constant. It also assumes that you fire all of your shield cell banks prior to your shields being lost. </p>
At the bottom of this panel you can change your engagement range. The engagement range is the distance between your ship and your target. Many weapons suffer from what is known as damage falloff, where their effectiveness decreases the further the distance between your ship and your target. This allows you to model the effect of engaging at different ranges. The time the shields will recover in is the time it will take your shields to go from collapsed (0%) to recovered (50%). This is affected by the number of pips you have in your SYS capacitor. </p>
Note that this panel only shows enabled weapons, so if you want to see your overall effectiveness for a subset of your weapons you can disable the undesired weapons in the power management panel.</p> The time the shields will recharge in is the time it will take your shields to go from recovered (50%) to full (100%). This is affected by the number of pips you have in your SYS capacitor. </p>
At the bottom of this panel are two graphs showing how your sustained DPS changes with engagement range. This shows at a glance how effective each weapon is at different distances.</p> </h3>Shield Sources</h3>
This chart provides information about the sources of your shields. For each applicable source of shields (generator, boosters, shield cell banks) a value is provided. </p>
<h2>Damage Received</h2> </h3>Damage Taken</h3>
The damage received panel provides information about the effectiveness of your build&apos;s defences against opponent&apos;s weapons at different engagement range. Features and functions are the same as the damage dealt panel, except that it does take in to account your build&apos;s resistances.</p> This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the shields. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values. </p>
</h3>Effective Shield</h3>
This graph shows the effective shield for each damage type, found by dividing the raw shield value by the damage taken for that type. </p>
<h3>Amour Metrics</h3>
The armour metrics provides information about your armour defence. </p>
Raw armour strength is the sum of the armour from your bulkheads and hull reinforcement packages. A tooltip provides a breakdown of these values. </p>
The time the armour will hold for is the time it will take your opponent&apos; to take your armour to 0. This assumes that they have 100% time on target, the engagement range stays constant, and that all damage is dealt to the armour rather than modules. </p>
Raw module armour is the sum of the protection from your module reinforcement packages. </p>
Protection for hardpoints is the amount of protection that your module reinforcement packages provide to hardpoints. This percentage of damage to the hardpoints will be diverted to the module reinforcement packages. </p>
Protection for all other modules is the amount of protection that your module reinforcement packages provide to everything other than hardpoints. This percentage of damage to the modules will be diverted to the module reinforcement packages. </p>
</h3>Armour Sources</h3>
This chart provides information about the sources of your armour. For each applicable source of shields (bulkheads, hull reinforcement packages) a value is provided. </p>
</h3>Damage Taken</h3>
This graph shows how the initial damage from the weapons of each type are reduced before their damage is applied to the armour. For each type of damage (absolute, explosive, kinetic, thermal) a percentage of the initial damage is provided. A tooltip provides a breakdown of these values. </p>
</h3>Effective Armour</h3>
This graph shows the effective armour for each damage type, found by dividing the raw armour value by the damage taken for that type. </p>
<h1>Keyboard Shortcuts</h1> <h1>Keyboard Shortcuts</h1>
<dl> <dl>
<dt>Ctrl-b</dt><dd>toggle boost</dd>
<dt>Ctrl-e</dt><dd>open export dialogue (outfitting page only)</dd> <dt>Ctrl-e</dt><dd>open export dialogue (outfitting page only)</dd>
<dt>Ctrl-h</dt><dd>open help dialogue</dd> <dt>Ctrl-h</dt><dd>open help dialogue</dd>
<dt>Ctrl-i</dt><dd>open import dialogue</dd> <dt>Ctrl-i</dt><dd>open import dialogue</dd>
<dt>Ctrl-o</dt><dd>open shortlink dialogue</dd> <dt>Ctrl-o</dt><dd>open shortlink dialogue</dd>
<dt>Ctrl-left-arrow</dt><dd>increase SYS capacitor</dd>
<dt>Ctrl-up-arrow</dt><dd>increase ENG capacitor</dd>
<dt>Ctrl-right-arrow</dt><dd>increase WEP capacitor</dd>
<dt>Ctrl-down-arrow</dt><dd>reset power distributor</dd>
<dt>Esc</dt><dd>close any open dialogue</dd> <dt>Esc</dt><dd>close any open dialogue</dd>
</dl> </dl>
<h1>Glossary</h1> <h1>Glossary</h1>

View File

@@ -1,32 +1,31 @@
import React from 'react'; import React from 'react';
// import Perf from 'react-addons-perf';
import { findDOMNode } from 'react-dom'; import { findDOMNode } from 'react-dom';
import { Ships } from 'coriolis-data/dist'; import { Ships } from 'coriolis-data/dist';
import cn from 'classnames'; import cn from 'classnames';
import Page from './Page'; import Page from './Page';
import Router from '../Router'; import Router from '../Router';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import * as Utils from '../utils/UtilityFunctions';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer'; import { toDetailedBuild } from '../shipyard/Serializer';
import { outfitURL } from '../utils/UrlGenerators'; import { outfitURL } from '../utils/UrlGenerators';
import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons'; import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons';
import LZString from 'lz-string';
import ShipSummaryTable from '../components/ShipSummaryTable'; import ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection'; import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection'; import HardpointsSlotSection from '../components/HardpointsSlotSection';
import InternalSlotSection from '../components/InternalSlotSection'; import InternalSlotSection from '../components/InternalSlotSection';
import UtilitySlotSection from '../components/UtilitySlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection';
import OffenceSummary from '../components/OffenceSummary'; import Pips from '../components/Pips';
import DefenceSummary from '../components/DefenceSummary'; import Boost from '../components/Boost';
import MovementSummary from '../components/MovementSummary'; import Fuel from '../components/Fuel';
import EngineProfile from '../components/EngineProfile'; import Cargo from '../components/Cargo';
import FSDProfile from '../components/FSDProfile'; import ShipPicker from '../components/ShipPicker';
import JumpRange from '../components/JumpRange'; import EngagementRange from '../components/EngagementRange';
import DamageDealt from '../components/DamageDealt'; import OutfittingSubpages from '../components/OutfittingSubpages';
import DamageReceived from '../components/DamageReceived';
import PowerManagement from '../components/PowerManagement';
import CostSection from '../components/CostSection';
import ModalExport from '../components/ModalExport'; import ModalExport from '../components/ModalExport';
import ModalPermalink from '../components/ModalPermalink'; import ModalPermalink from '../components/ModalPermalink';
import Slider from '../components/Slider';
/** /**
* Document Title Generator * Document Title Generator
@@ -50,17 +49,25 @@ export default class OutfittingPage extends Page {
*/ */
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this.state = this._initState(context); // window.Perf = Perf;
this.state = this._initState(props, context);
this._keyDown = this._keyDown.bind(this); this._keyDown = this._keyDown.bind(this);
this._exportBuild = this._exportBuild.bind(this); this._exportBuild = this._exportBuild.bind(this);
this._pipsUpdated = this._pipsUpdated.bind(this);
this._boostUpdated = this._boostUpdated.bind(this);
this._cargoUpdated = this._cargoUpdated.bind(this);
this._fuelUpdated = this._fuelUpdated.bind(this);
this._opponentUpdated = this._opponentUpdated.bind(this);
this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this);
} }
/** /**
* [Re]Create initial state from context * [Re]Create initial state from context
* @param {Object} props React component properties
* @param {context} context React component context * @param {context} context React component context
* @return {Object} New state object * @return {Object} New state object
*/ */
_initState(context) { _initState(props, context) {
let params = context.route.params; let params = context.route.params;
let shipId = params.ship; let shipId = params.ship;
let code = params.code; let code = params.code;
@@ -82,6 +89,8 @@ export default class OutfittingPage extends Page {
this._getTitle = getTitle.bind(this, data.properties.name); this._getTitle = getTitle.bind(this, data.properties.name);
// Obtain ship control from code
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code);
return { return {
error: null, error: null,
title: this._getTitle(buildName), title: this._getTitle(buildName),
@@ -91,7 +100,19 @@ export default class OutfittingPage extends Page {
shipId, shipId,
ship, ship,
code, code,
savedCode savedCode,
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
opponentSys,
opponentEng,
opponentWep,
engagementRange
}; };
} }
@@ -113,35 +134,199 @@ export default class OutfittingPage extends Page {
this.setState(stateChanges); this.setState(stateChanges);
} }
/**
* Update the control part of the route
*/
_updateRouteOnControlChange() {
const { ship, shipId, buildName } = this.state;
const code = this._fullCode(ship);
this._updateRoute(shipId, buildName, code);
this.setState({ code });
}
/**
* Provide a full code for this ship, including any additions due to the outfitting page
* @param {Object} ship the ship
* @param {number} fuel the fuel carried by the ship (if different from that in state)
* @param {number} cargo the cargo carried by the ship (if different from that in state)
* @returns {string} the code for this ship
*/
_fullCode(ship, fuel, cargo) {
return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`;
}
/**
* Obtain the control information from the build code
* @param {Object} ship The ship
* @param {string} code The build code
* @returns {Object} The control information
*/
_obtainControlFromCode(ship, code) {
// Defaults
let sys = 2;
let eng = 2;
let wep = 2;
let boost = false;
let fuel = ship.fuelCapacity;
let cargo = ship.cargoCapacity;
let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults);
let opponentSys = 2;
let opponentEng = 2;
let opponentWep = 2;
let opponentBuild;
let engagementRange = 1000;
// Obtain updates from code, if available
if (code) {
const parts = code.split('.');
if (parts.length >= 5) {
// We have control information in the code
const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/');
sys = parseFloat(control[0]);
eng = parseFloat(control[1]);
wep = parseFloat(control[2]);
boost = control[3] == 1 ? true : false;
fuel = parseFloat(control[4]);
cargo = parseInt(control[5]);
if (control[6]) {
const shipId = control[6];
opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
if (control[7] && Persist.getBuild(shipId, control[7])) {
// Ship is a particular build
const opponentCode = Persist.getBuild(shipId, control[7]);
opponent.buildFrom(opponentCode);
opponentBuild = control[7];
if (opponentBuild) {
// Obtain opponent's sys/eng/wep pips from their code
const opponentParts = opponentCode.split('.');
if (opponentParts.length >= 5) {
const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
opponentSys = parseFloat(opponentControl[0]);
opponentEng = parseFloat(opponentControl[1]);
opponentWep = parseFloat(opponentControl[2]);
}
}
} else {
// Ship is a stock build
opponent.buildWith(Ships[shipId].defaults);
}
}
engagementRange = parseInt(control[8]);
}
}
return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange };
}
/**
* Triggered when pips have been updated
* @param {number} sys SYS pips
* @param {number} eng ENG pips
* @param {number} wep WEP pips
*/
_pipsUpdated(sys, eng, wep) {
this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when boost has been updated
* @param {boolean} boost true if boosting
*/
_boostUpdated(boost) {
this.setState({ boost }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when fuel has been updated
* @param {number} fuel the amount of fuel, in T
*/
_fuelUpdated(fuel) {
this.setState({ fuel }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when cargo has been updated
* @param {number} cargo the amount of cargo, in T
*/
_cargoUpdated(cargo) {
this.setState({ cargo }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when engagement range has been updated
* @param {number} engagementRange the engagement range, in m
*/
_engagementRangeUpdated(engagementRange) {
this.setState({ engagementRange }, () => this._updateRouteOnControlChange());
}
/**
* Triggered when target ship has been updated
* @param {string} opponent the opponent's ship model
* @param {string} opponentBuild the name of the opponent's build
*/
_opponentUpdated(opponent, opponentBuild) {
const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots);
if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) {
// Ship is a particular build
opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild));
} else {
// Ship is a stock build
opponentShip.buildWith(Ships[opponent].defaults);
}
this.setState({ opponent: opponentShip, opponentBuild }, () => this._updateRouteOnControlChange());
}
/**
* Set the control code for this outfitting page
* @param {number} fuel the fuel carried by the ship (if different from that in state)
* @param {number} cargo the cargo carried by the ship (if different from that in state)
* @returns {string} The control code
*/
_controlCode(fuel, cargo) {
const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state;
const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`;
return code;
}
/** /**
* Save the current build * Save the current build
*/ */
_saveBuild() { _saveBuild() {
let code = this.state.ship.toString(); const { code, buildName, newBuildName, shipId } = this.state;
let { buildName, newBuildName, shipId } = this.state;
if (buildName === newBuildName) { Persist.saveBuild(shipId, newBuildName, code);
Persist.saveBuild(shipId, buildName, code); this._updateRoute(shipId, newBuildName, code);
this._updateRoute(shipId, buildName, code);
let opponent, opponentBuild, opponentSys, opponentEng, opponentWep;
if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
// This is a save of our current opponent build; update it
opponentBuild = newBuildName;
opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code);
opponentSys = this.state.sys;
opponentEng = this.state.eng;
opponentWep = this.state.wep;
} else { } else {
Persist.saveBuild(shipId, newBuildName, code); opponentBuild = this.state.opponentBuild;
this._updateRoute(shipId, newBuildName, code); opponent = this.state.opponent;
opponentSys = this.state.opponentSys;
opponentEng = this.state.opponentEng;
opponentWep = this.state.opponentWep;
} }
this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) });
this.setState({ buildName: newBuildName, code, savedCode: code, title: this._getTitle(newBuildName) });
} }
/** /**
* Rename the current build * Rename the current build
*/ */
_renameBuild() { _renameBuild() {
let { buildName, newBuildName, shipId, ship } = this.state; const { code, buildName, newBuildName, shipId, ship } = this.state;
if (buildName != newBuildName && newBuildName.length) { if (buildName != newBuildName && newBuildName.length) {
let code = ship.toString();
Persist.deleteBuild(shipId, buildName); Persist.deleteBuild(shipId, buildName);
Persist.saveBuild(shipId, newBuildName, code); Persist.saveBuild(shipId, newBuildName, code);
this._updateRoute(shipId, newBuildName, code); this._updateRoute(shipId, newBuildName, code);
this.setState({ buildName: newBuildName, code, savedCode: code }); this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName });
} }
} }
@@ -149,24 +334,50 @@ export default class OutfittingPage extends Page {
* Reload build from last save * Reload build from last save
*/ */
_reloadBuild() { _reloadBuild() {
this.state.ship.buildFrom(this.state.savedCode); this.setState({ code: this.state.savedCode }, () => this._codeUpdated());
this._shipUpdated();
} }
/** /**
* Reset build to Stock/Factory defaults * Reset build to Stock/Factory defaults
*/ */
_resetBuild() { _resetBuild() {
this.state.ship.buildWith(Ships[this.state.shipId].defaults); const { ship, shipId, buildName } = this.state;
this._shipUpdated(); // Rebuild ship
ship.buildWith(Ships[shipId].defaults);
// Reset controls
const code = ship.toString();
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
// Update state, and refresh the ship
this.setState({
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
}, () => this._updateRoute(shipId, buildName, code));
} }
/** /**
* Delete the build * Delete the build
*/ */
_deleteBuild() { _deleteBuild() {
Persist.deleteBuild(this.state.shipId, this.state.buildName); const { shipId, buildName } = this.state;
Persist.deleteBuild(shipId, buildName);
let opponentBuild;
if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
// Our current opponent has been deleted; revert to stock
opponentBuild = null;
} else {
opponentBuild = this.state.opponentBuild;
}
Router.go(outfitURL(this.state.shipId)); Router.go(outfitURL(this.state.shipId));
this.setState({ opponentBuild });
} }
/** /**
@@ -183,14 +394,43 @@ export default class OutfittingPage extends Page {
} }
/** /**
* Trigger render on ship model change * Called when the code for the ship has been updated, to synchronise the rest of the data
*/
_codeUpdated() {
const { code, ship, shipId, buildName } = this.state;
// Rebuild ship from the code
this.state.ship.buildFrom(code);
// Obtain controls from the code
const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
// Update state, and refresh the route when complete
this.setState({
sys,
eng,
wep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
}, () => this._updateRoute(shipId, buildName, code));
}
/**
* Called when the ship has been updated, to set the code and then update accordingly
*/ */
_shipUpdated() { _shipUpdated() {
let { shipId, buildName, ship } = this.state; let { ship, shipId, buildName, cargo, fuel } = this.state;
let code = ship.toString(); if (cargo > ship.cargoCapacity) {
cargo = ship.cargoCapacity;
this._updateRoute(shipId, buildName, code); }
this.setState({ code }); if (fuel > ship.fuelCapacity) {
fuel = ship.fuelCapacity;
}
const code = this._fullCode(ship, fuel, cargo);
this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code));
} }
/** /**
@@ -203,20 +443,6 @@ export default class OutfittingPage extends Page {
Router.replace(outfitURL(shipId, code, buildName)); Router.replace(outfitURL(shipId, code, buildName));
} }
/**
* Update dimenions from rendered DOM
*/
_updateDimensions() {
let elem = findDOMNode(this.refs.chartThird);
if (elem) {
this.setState({
thirdChartWidth: findDOMNode(this.refs.chartThird).offsetWidth,
halfChartWidth: findDOMNode(this.refs.chartThird).offsetWidth * 3 / 2
});
}
}
/** /**
* Update state based on context changes * Update state based on context changes
* @param {Object} nextProps Incoming/Next properties * @param {Object} nextProps Incoming/Next properties
@@ -224,7 +450,7 @@ export default class OutfittingPage extends Page {
*/ */
componentWillReceiveProps(nextProps, nextContext) { componentWillReceiveProps(nextProps, nextContext) {
if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
this.setState(this._initState(nextContext)); this.setState(this._initState(nextProps, nextContext));
} }
} }
@@ -232,22 +458,14 @@ export default class OutfittingPage extends Page {
* Add listeners when about to mount * Add listeners when about to mount
*/ */
componentWillMount() { componentWillMount() {
this.resizeListener = this.context.onWindowResize(this._updateDimensions);
document.addEventListener('keydown', this._keyDown); document.addEventListener('keydown', this._keyDown);
} }
/**
* Trigger DOM updates on mount
*/
componentDidMount() {
this._updateDimensions();
}
/** /**
* Remove listeners on unmount * Remove listeners on unmount
*/ */
componentWillUnmount() { componentWillUnmount() {
this.resizeListener.remove(); document.removeEventListener('keydown', this._keyDown);
} }
/** /**
@@ -295,19 +513,30 @@ export default class OutfittingPage extends Page {
let state = this.state, let state = this.state,
{ language, termtip, tooltip, sizeRatio, onWindowResize } = this.context, { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context,
{ translate, units, formats } = language, { translate, units, formats } = language,
{ ship, code, savedCode, buildName, newBuildName, halfChartWidth, thirdChartWidth } = state, { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state,
hide = tooltip.bind(null, null), hide = tooltip.bind(null, null),
menu = this.props.currentMenu, menu = this.props.currentMenu,
shipUpdated = this._shipUpdated, shipUpdated = this._shipUpdated,
canSave = (newBuildName || buildName) && code !== savedCode, canSave = (newBuildName || buildName) && code !== savedCode,
canRename = buildName && newBuildName && buildName != newBuildName, canRename = buildName && newBuildName && buildName != newBuildName,
canReload = savedCode && canSave, canReload = savedCode && canSave;
hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(),
iStr = ship.getInternalString() + '.' + ship.getModificationsString();
// Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up // Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up
code = ship.name + (code || ''); code = ship.name + (code || '');
// Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time
const _sStr = ship.getStandardString();
const _iStr = ship.getInternalString();
const _hStr = ship.getHardpointsString();
const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`;
const _mStr = ship.getModificationsString();
const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}`;
const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`;
const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`;
const boostMarker = `${ship.canBoost()}`;
const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}`;
return ( return (
<div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}> <div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}>
<div id='overview'> <div id='overview'>
@@ -341,46 +570,63 @@ export default class OutfittingPage extends Page {
</div> </div>
</div> </div>
<ShipSummaryTable ship={ship} code={code} /> {/* Main tables */}
<StandardSlotSection ship={ship} code={code} onChange={shipUpdated} currentMenu={menu} /> <ShipSummaryTable ship={ship} marker={shipSummaryMarker} />
<InternalSlotSection ship={ship} code={iStr} onChange={shipUpdated} currentMenu={menu} /> <StandardSlotSection ship={ship} code={standardSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<HardpointsSlotSection ship={ship} code={hStr || ''} onChange={shipUpdated} currentMenu={menu} /> <InternalSlotSection ship={ship} code={internalSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hStr || ''} onChange={shipUpdated} currentMenu={menu} /> <HardpointsSlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<UtilitySlotSection ship={ship} code={hardpointsSlotMarker} onChange={shipUpdated} currentMenu={menu} />
<div ref='chartThird' className='group third'> {/* Control of ship and opponent */}
<OffenceSummary ship={ship} code={code}/> <div className='group quarter'>
<div className='group half'>
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('ship control')}</h2>
</div>
<div className='group half'>
<Boost marker={boostMarker} ship={ship} boost={boost} onChange={this._boostUpdated} />
</div>
</div> </div>
<div className='group third'> <div className='group quarter'>
<DefenceSummary ship={ship} code={code}/> <Pips sys={sys} eng={eng} wep={wep} onChange={this._pipsUpdated} />
</div> </div>
<div className='group third'> <div className='group quarter'>
<MovementSummary ship={ship} code={code}/> <Fuel fuelCapacity={ship.fuelCapacity} fuel={fuel} onChange={this._fuelUpdated}/>
</div>
<div className='group quarter'>
{ ship.cargoCapacity > 0 ? <Cargo cargoCapacity={ship.cargoCapacity} cargo={cargo} onChange={this._cargoUpdated}/> : null }
</div>
<div className='group half'>
<div className='group quarter'>
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>{translate('opponent')}</h2>
</div>
<div className='group threequarters'>
<ShipPicker ship={opponent.id} build={opponentBuild} onChange={this._opponentUpdated}/>
</div>
</div>
<div className='group half'>
<EngagementRange ship={ship} engagementRange={engagementRange} onChange={this._engagementRangeUpdated}/>
</div> </div>
<PowerManagement ship={ship} code={code} onChange={shipUpdated} /> {/* Tabbed subpages */}
<CostSection ship={ship} buildName={buildName} code={code} /> <OutfittingSubpages
ship={ship}
<div className='group third'> code={code}
<EngineProfile ship={ship} code={code} chartWidth={thirdChartWidth} /> buildName={buildName}
</div> onChange={shipUpdated}
sys={sys}
<div className='group third'> eng={eng}
<FSDProfile ship={ship} code={code} chartWidth={thirdChartWidth} /> wep={wep}
</div> boost={boost}
cargo={cargo}
<div className='group third'> fuel={fuel}
<JumpRange ship={ship} code={code} chartWidth={thirdChartWidth} /> engagementRange={engagementRange}
</div> opponent={opponent}
opponentBuild={opponentBuild}
<div> opponentSys={opponentSys}
<DamageDealt ship={ship} code={code} chartWidth={halfChartWidth} currentMenu={menu}/> opponentEng={opponentEng}
</div> opponentWep={opponentWep}
/>
<div>
<DamageReceived ship={ship} code={code} currentMenu={menu}/>
</div>
</div> </div>
); );
} }
} }

View File

@@ -9,33 +9,33 @@ import Module from './Module';
* @return {number} Distance in Light Years * @return {number} Distance in Light Years
*/ */
export function jumpRange(mass, fsd, fuel) { export function jumpRange(mass, fsd, fuel) {
let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass;
} }
/** /**
* Calculate the fastest (total) range based on mass and a specific FSD, and all fuel available * Calculate the total jump range based on mass and a specific FSD, and all fuel available
* *
* @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc
* @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass
* @param {number} fuel The total fuel available * @param {number} fuel The total fuel available
* @return {number} Distance in Light Years * @return {number} Distance in Light Years
*/ */
export function fastestRange(mass, fsd, fuel) { export function totalJumpRange(mass, fsd, fuel) {
let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; const fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel;
let fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass; const fsdOptimalMass = fsd instanceof Module ? fsd.getOptMass() : fsd.optmass;
let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps
let jumps = Math.floor(fuel / fsdMaxFuelPerJump); let fuelRemaining = fuel;
mass += fuelRemaining; let totalRange = 0;
// Going backwards, start with the last jump using the remaining fuel while (fuelRemaining > 0) {
let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0; const fuelForThisJump = Math.min(fuelRemaining, fsdMaxFuelPerJump);
// For each max fuel jump, calculate the max jump range based on fuel mass left in the tank totalRange += this.jumpRange(mass, fsd, fuelForThisJump);
for (let j = 0; j < jumps; j++) { // Mass is reduced
mass += fsd.maxfuel; mass -= fuelForThisJump;
fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; fuelRemaining -= fuelForThisJump;
} }
return fastestRange; return totalRange;
}; };
/** /**
@@ -173,3 +173,681 @@ function normValues(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, bas
res * (1 - (engpip * 1)), res * (1 - (engpip * 1)),
res]; res];
} }
/**
* Calculate a single value
* @param {number} minMass the minimum mass of the thrusters
* @param {number} optMass the optimum mass of the thrusters
* @param {number} maxMass the maximum mass of the thrusters
* @param {number} minMul the minimum multiplier of the thrusters
* @param {number} optMul the optimum multiplier of the thrusters
* @param {number} maxMul the maximum multiplier of the thrusters
* @param {number} mass the mass of the ship
* @param {base} base the base value from which to calculate
* @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines
* @returns {number} the resultant value
*/
function calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, base, engpip, eng) {
const xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass));
const exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass)));
const ynorm = Math.pow(xnorm, exponent);
const mul = minMul + ynorm * (maxMul - minMul);
const res = base * mul;
return res * (1 - (engpip * (4 - eng)));
}
/**
* Calculate speed for a given setup
* @param {number} mass the mass of the ship
* @param {number} baseSpeed the base speed of the ship
* @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship
* @param {boolean} boost true if the boost is activated
* @returns {number} the resultant speed
*/
export function calcSpeed(mass, baseSpeed, thrusters, engpip, eng, boostFactor, boost) {
// thrusters might be a module or a template; handle either here
const minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
const optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
const maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
const minMul = thrusters instanceof Module ? thrusters.getMinMul('speed') : (thrusters.minmulspeed ? thrusters.minmulspeed : thrusters.minmul);
const optMul = thrusters instanceof Module ? thrusters.getOptMul('speed') : (thrusters.optmulspeed ? thrusters.minmulspeed : thrusters.minmul);
const maxMul = thrusters instanceof Module ? thrusters.getMaxMul('speed') : (thrusters.maxmulspeed ? thrusters.minmulspeed : thrusters.minmul);
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseSpeed, engpip, eng);
if (boost == true) {
result *= boostFactor;
}
return result;
}
/**
* Calculate pitch for a given setup
* @param {number} mass the mass of the ship
* @param {number} basePitch the base pitch of the ship
* @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship
* @param {boolean} boost true if the boost is activated
* @returns {number} the resultant pitch
*/
export function calcPitch(mass, basePitch, thrusters, engpip, eng, boostFactor, boost) {
// thrusters might be a module or a template; handle either here
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, basePitch, engpip, eng);
if (boost == true) {
result *= boostFactor;
}
return result;
}
/**
* Calculate roll for a given setup
* @param {number} mass the mass of the ship
* @param {number} baseRoll the base roll of the ship
* @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship
* @param {boolean} boost true if the boost is activated
* @returns {number} the resultant roll
*/
export function calcRoll(mass, baseRoll, thrusters, engpip, eng, boostFactor, boost) {
// thrusters might be a module or a template; handle either here
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseRoll, engpip, eng);
if (boost == true) {
result *= boostFactor;
}
return result;
}
/**
* Calculate yaw for a given setup
* @param {number} mass the mass of the ship
* @param {number} baseYaw the base yaw of the ship
* @param {ojbect} thrusters the thrusters of the ship
* @param {number} engpip the multiplier per pip to engines
* @param {number} eng the pips to engines
* @param {number} boostFactor the boost factor for ths ship
* @param {boolean} boost true if the boost is activated
* @returns {number} the resultant yaw
*/
export function calcYaw(mass, baseYaw, thrusters, engpip, eng, boostFactor, boost) {
// thrusters might be a module or a template; handle either here
let minMass = thrusters instanceof Module ? thrusters.getMinMass() : thrusters.minmass;
let optMass = thrusters instanceof Module ? thrusters.getOptMass() : thrusters.optmass;
let maxMass = thrusters instanceof Module ? thrusters.getMaxMass() : thrusters.maxmass;
let minMul = thrusters instanceof Module ? thrusters.getMinMul('rotation') : (thrusters.minmulrotation ? thrusters.minmulrotation : thrusters.minmul);
let optMul = thrusters instanceof Module ? thrusters.getOptMul('rotation') : (thrusters.optmulrotation ? thrusters.optmulrotation : thrusters.optmul);
let maxMul = thrusters instanceof Module ? thrusters.getMaxMul('rotation') : (thrusters.maxmulrotation ? thrusters.maxmulrotation : thrusters.maxmul);
let result = calcValue(minMass, optMass, maxMass, minMul, optMul, maxMul, mass, baseYaw, engpip, eng);
if (boost == true) {
result *= boostFactor;
}
return result;
}
/**
* Calculate shield metrics
* @param {Object} ship The ship
* @param {int} sys The pips to SYS
* @returns {Object} Shield metrics
*/
export function shieldMetrics(ship, sys) {
const sysResistance = this.sysResistance(sys);
const maxSysResistance = this.sysResistance(4);
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 = this.shieldStrength(ship.hullMass, ship.baseShieldStrength, shieldGenerator, 1);
const boostersStrength = generatorStrength * boost;
// Recover time is the time taken to go from 0 to 50%. It includes a 16-second wait before shields start to recover
const shieldToRecover = (generatorStrength + boostersStrength) / 2;
const powerDistributor = ship.standard[4].m;
const sysRechargeRate = this.sysRechargeRate(powerDistributor, sys);
// Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes
// 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration
let capacitorDrain = (shieldGenerator.getBrokenRegenerationRate() * 0.6) - sysRechargeRate;
let capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain;
let recover = 16;
if (capacitorDrain <= 0 || shieldToRecover < capacitorLifetime * shieldGenerator.getBrokenRegenerationRate()) {
// We can recover the entire shield from the capacitor store
recover += shieldToRecover / shieldGenerator.getBrokenRegenerationRate();
} else {
// We can recover some of the shield from the capacitor store
recover += capacitorLifetime;
const remainingShieldToRecover = shieldToRecover - capacitorLifetime * shieldGenerator.getBrokenRegenerationRate();
if (sys === 0) {
// No system pips so will never recover shields
recover = Math.Inf;
} else {
// Recover remaining shields at the rate of the power distributor's recharge
recover += remainingShieldToRecover / (sysRechargeRate / 0.6);
}
}
// Recharge time is the time taken to go from 50% to 100%
const shieldToRecharge = (generatorStrength + boostersStrength) / 2;
// Our initial regeneration comes from the SYS capacitor store, which is replenished as it goes
// 0.6 is a magic number from FD: each 0.6 MW of energy from the power distributor recharges 1 MJ/s of regeneration
capacitorDrain = (shieldGenerator.getRegenerationRate() * 0.6) - sysRechargeRate;
capacitorLifetime = powerDistributor.getSystemsCapacity() / capacitorDrain;
let recharge = 0;
if (capacitorDrain <= 0 || shieldToRecharge < capacitorLifetime * shieldGenerator.getRegenerationRate()) {
// We can recharge the entire shield from the capacitor store
recharge += shieldToRecharge / shieldGenerator.getRegenerationRate();
} else {
// We can recharge some of the shield from the capacitor store
recharge += capacitorLifetime;
const remainingShieldToRecharge = shieldToRecharge - capacitorLifetime * shieldGenerator.getRegenerationRate();
if (sys === 0) {
// No system pips so will never recharge shields
recharge = Math.Inf;
} else {
// Recharge remaining shields at the rate of the power distributor's recharge
recharge += remainingShieldToRecharge / (sysRechargeRate / 0.6);
}
}
shield = {
generator: generatorStrength,
boosters: boostersStrength,
cells: ship.shieldCells,
total: generatorStrength + boostersStrength + ship.shieldCells,
recover,
recharge,
};
// 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,
max: 1 - maxSysResistance
};
shield.explosive = {
generator: 1 - shieldGenerator.getExplosiveResistance(),
boosters: boosterExplDmg,
sys: (1 - sysResistance),
total: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - sysResistance),
max: (1 - shieldGenerator.getExplosiveResistance()) * boosterExplDmg * (1 - maxSysResistance)
};
shield.kinetic = {
generator: 1 - shieldGenerator.getKineticResistance(),
boosters: boosterKinDmg,
sys: (1 - sysResistance),
total: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - sysResistance),
max: (1 - shieldGenerator.getKineticResistance()) * boosterKinDmg * (1 - maxSysResistance)
};
shield.thermal = {
generator: 1 - shieldGenerator.getThermalResistance(),
boosters: boosterThermDmg,
sys: (1 - sysResistance),
total: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - sysResistance),
max: (1 - shieldGenerator.getThermalResistance()) * boosterThermDmg * (1 - maxSysResistance)
};
}
return shield;
}
/**
* Calculate armour metrics
* @param {Object} ship The ship
* @returns {Object} Armour metrics
*/
export function armourMetrics(ship) {
// 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
};
return armour;
}
/**
* Calculate defence metrics for a ship
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {int} sys The pips to SYS
* @param {int} opponentWep The pips to pponent's WEP
* @param {int} engagementrange The range between the ship and opponent
* @returns {Object} Defence metrics
*/
export function defenceMetrics(ship, opponent, sys, opponentWep, engagementrange) {
// Obtain the shield metrics
const shield = this.shieldMetrics(ship, sys);
// Obtain the armour metrics
const armour = this.armourMetrics(ship);
// Obtain the opponent's sustained DPS on us
const sustainedDps = this.sustainedDps(opponent, ship, sys, engagementrange);
const shielddamage = shield.generator ? {
absolutesdps: sustainedDps.shieldsdps.absolute,
explosivesdps: sustainedDps.shieldsdps.explosive,
kineticsdps: sustainedDps.shieldsdps.kinetic,
thermalsdps: sustainedDps.shieldsdps.thermal,
totalsdps: sustainedDps.shieldsdps.absolute + sustainedDps.shieldsdps.explosive + sustainedDps.shieldsdps.kinetic + sustainedDps.shieldsdps.thermal,
totalseps: sustainedDps.eps
} : {};
const armourdamage = {
absolutesdps: sustainedDps.armoursdps.absolute,
explosivesdps: sustainedDps.armoursdps.explosive,
kineticsdps: sustainedDps.armoursdps.kinetic,
thermalsdps: sustainedDps.armoursdps.thermal,
totalsdps: sustainedDps.armoursdps.absolute + sustainedDps.armoursdps.explosive + sustainedDps.armoursdps.kinetic + sustainedDps.armoursdps.thermal,
totalseps: sustainedDps.eps
};
return { shield, armour, shielddamage, armourdamage };
}
/**
* Calculate offence metrics for a ship
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {int} wep The pips to WEP
* @param {int} opponentSys The pips to opponent's SYS
* @param {int} engagementrange The range between the ship and opponent
* @returns {array} Offence metrics
*/
export function offenceMetrics(ship, opponent, wep, opponentSys, engagementrange) {
// Per-weapon and total damage
const damage = [];
// Obtain the opponent's shield and armour metrics
const opponentShields = this.shieldMetrics(opponent, opponentSys);
const opponentArmour = this.armourMetrics(opponent);
// Per-weapon and total damage to shields
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
const classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
let engineering;
if (m.blueprint && m.blueprint.name) {
engineering = m.blueprint.name + ' ' + 'grade' + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id >= 0) {
engineering += ', ' + m.blueprint.special.name;
}
}
const weaponSustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange);
damage.push({
id: i,
mount: m.mount,
name: m.name || m.grp,
classRating,
engineering,
sdps: weaponSustainedDps.damage,
seps: weaponSustainedDps.eps,
effectiveness: weaponSustainedDps.effectiveness
});
}
}
return damage;
}
/**
* Calculate the resistance provided by SYS pips
* @param {integer} sys the value of the SYS pips
* @returns {integer} the resistance for the given pips
*/
export function sysResistance(sys) {
return Math.pow(sys, 0.85) * 0.6 / Math.pow(4, 0.85);
}
/**
* Obtain the recharge rate of the SYS capacitor of a power distributor given pips
* @param {Object} pd The power distributor
* @param {number} sys The number of pips to SYS
* @returns {number} The recharge rate in MJ/s
*/
export function sysRechargeRate(pd, sys) {
return pd.getSystemsRechargeRate() * Math.pow(sys, 1.1) / Math.pow(4, 1.1);
}
/**
* Calculate the sustained DPS for a ship against an opponent at a given range
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {number} sys Pips to opponent's SYS
* @param {int} engagementrange The range between the ship and opponent
* @returns {Object} Sustained DPS for shield and armour
*/
export function sustainedDps(ship, opponent, sys, engagementrange) {
// Obtain the opponent's shield and armour metrics
const opponentShields = this.shieldMetrics(opponent, sys);
const opponentArmour = this.armourMetrics(opponent);
return this._sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange);
}
/**
* Calculate the sustained DPS for a ship against an opponent at a given range
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {Object} opponentShields The opponent's shield resistances
* @param {Object} opponentArmour The opponent's armour resistances
* @param {int} engagementrange The range between the ship and opponent
* @returns {Object} Sustained DPS for shield and armour
*/
export function _sustainedDps(ship, opponent, opponentShields, opponentArmour, engagementrange) {
const shieldsdps = {
absolute: 0,
explosive: 0,
kinetic: 0,
thermal: 0
};
const armoursdps = {
absolute: 0,
explosive: 0,
kinetic: 0,
thermal: 0
};
let eps = 0;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].m && ship.hardpoints[i].enabled && ship.hardpoints[i].maxClass > 0) {
const m = ship.hardpoints[i].m;
const sustainedDps = this._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange);
shieldsdps.absolute += sustainedDps.damage.shields.absolute;
shieldsdps.explosive += sustainedDps.damage.shields.explosive;
shieldsdps.kinetic += sustainedDps.damage.shields.kinetic;
shieldsdps.thermal += sustainedDps.damage.shields.thermal;
armoursdps.absolute += sustainedDps.damage.armour.absolute;
armoursdps.explosive += sustainedDps.damage.armour.explosive;
armoursdps.kinetic += sustainedDps.damage.armour.kinetic;
armoursdps.thermal += sustainedDps.damage.armour.thermal;
eps += sustainedDps.eps;
}
}
return { shieldsdps, armoursdps, eps };
}
/**
* Calculate the sustained DPS for a weapon at a given range
* @param {Object} m The weapon
* @param {Object} opponent The opponent ship
* @param {Object} opponentShields The opponent's shield resistances
* @param {Object} opponentArmour The opponent's armour resistances
* @param {int} engagementrange The range between the ship and opponent
* @returns {Object} Sustained DPS for shield and armour
*/
export function _weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementrange) {
const opponentHasShields = opponentShields.generator ? true : false;
const weapon = {
eps: 0,
damage: {
shields: {
absolute: 0,
explosive: 0,
kinetic: 0,
thermal: 0,
total: 0
},
armour: {
absolute: 0,
explosive: 0,
kinetic: 0,
thermal: 0,
total: 0
},
},
effectiveness: {
shields: {
range: 1,
sys: opponentHasShields ? opponentShields.absolute.sys : 1,
resistance: 1
},
armour: {
range: 1,
hardness: 1,
resistance: 1
}
}
};
// EPS
weapon.eps = m.getClip() ? (m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getEps();
// Initial sustained DPS
let sDps = m.getClip() ? (m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload()) : m.getDps();
// Take fall-off in to account
const falloff = m.getFalloff();
if (falloff && engagementrange > falloff) {
const dropoffRange = m.getRange() - falloff;
const dropoff = 1 - Math.min((engagementrange - falloff) / dropoffRange, 1);
weapon.effectiveness.shields.range = weapon.effectiveness.armour.range = dropoff;
sDps *= dropoff;
}
// Piercing/hardness modifier (for armour only)
const armourMultiple = m.getPiercing() >= opponent.hardness ? 1 : m.getPiercing() / opponent.hardness;
weapon.effectiveness.armour.hardness = armourMultiple;
// Break out the damage according to type
let shieldsResistance = 0;
let armourResistance = 0;
if (m.getDamageDist().A) {
weapon.damage.shields.absolute += sDps * m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.total : 1);
weapon.damage.armour.absolute += sDps * m.getDamageDist().A * armourMultiple * opponentArmour.absolute.total;
shieldsResistance += m.getDamageDist().A * (opponentHasShields ? opponentShields.absolute.generator * opponentShields.absolute.boosters : 1);
armourResistance += m.getDamageDist().A * opponentArmour.absolute.bulkheads * opponentArmour.absolute.reinforcement;
}
if (m.getDamageDist().E) {
weapon.damage.shields.explosive += sDps * m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.total : 1);
weapon.damage.armour.explosive += sDps * m.getDamageDist().E * armourMultiple * opponentArmour.explosive.total;
shieldsResistance += m.getDamageDist().E * (opponentHasShields ? opponentShields.explosive.generator * opponentShields.explosive.boosters : 1);
armourResistance += m.getDamageDist().E * opponentArmour.explosive.bulkheads * opponentArmour.explosive.reinforcement;
}
if (m.getDamageDist().K) {
weapon.damage.shields.kinetic += sDps * m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.total : 1);
weapon.damage.armour.kinetic += sDps * m.getDamageDist().K * armourMultiple * opponentArmour.kinetic.total;
shieldsResistance += m.getDamageDist().K * (opponentHasShields ? opponentShields.kinetic.generator * opponentShields.kinetic.boosters : 1);
armourResistance += m.getDamageDist().K * opponentArmour.kinetic.bulkheads * opponentArmour.kinetic.reinforcement;
}
if (m.getDamageDist().T) {
weapon.damage.shields.thermal += sDps * m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.total : 1);
weapon.damage.armour.thermal += sDps * m.getDamageDist().T * armourMultiple * opponentArmour.thermal.total;
shieldsResistance += m.getDamageDist().T * (opponentHasShields ? opponentShields.thermal.generator * opponentShields.thermal.boosters : 1);
armourResistance += m.getDamageDist().T * opponentArmour.thermal.bulkheads * opponentArmour.thermal.reinforcement;
}
weapon.damage.shields.total = weapon.damage.shields.absolute + weapon.damage.shields.explosive + weapon.damage.shields.kinetic + weapon.damage.shields.thermal;
weapon.damage.armour.total = weapon.damage.armour.absolute + weapon.damage.armour.explosive + weapon.damage.armour.kinetic + weapon.damage.armour.thermal;
weapon.effectiveness.shields.resistance *= shieldsResistance;
weapon.effectiveness.armour.resistance *= armourResistance;
weapon.effectiveness.shields.total = weapon.effectiveness.shields.range * weapon.effectiveness.shields.sys * weapon.effectiveness.shields.resistance;
weapon.effectiveness.armour.total = weapon.effectiveness.armour.range * weapon.effectiveness.armour.resistance * weapon.effectiveness.armour.hardness;
return weapon;
}
/**
* Calculate time to drain WEP capacitor
* @param {object} ship The ship
* @param {number} wep Pips to WEP
* @returns {number} The time to drain the WEP capacitor, in seconds
*/
export function timeToDrainWep(ship, wep) {
let totalSEps = 0;
for (let slotNum in ship.hardpoints) {
const slot = ship.hardpoints[slotNum];
if (slot.maxClass > 0 && slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) {
totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps();
}
}
// Calculate the drain time
const drainPerSecond = totalSEps - ship.standard[4].m.getWeaponsRechargeRate() * wep / 4;
if (drainPerSecond <= 0) {
// Can fire forever
return Infinity;
} else {
const initialCharge = ship.standard[4].m.getWeaponsCapacity();
return initialCharge / drainPerSecond;
}
}
/**
* Calculate the time to deplete an amount of shields or armour
* @param {number} amount The amount to be depleted
* @param {number} dps The depletion per second
* @param {number} eps The energy drained per second
* @param {number} capacity The initial energy capacity
* @param {number} recharge The energy recharged per second
* @returns {number} The number of seconds to deplete to 0
*/
export function timeToDeplete(amount, dps, eps, capacity, recharge) {
const drainPerSecond = eps - recharge;
if (drainPerSecond <= 0) {
// Simple result
return amount / dps;
} else {
// We are draining the capacitor, but can we deplete before we run out
const timeToDrain = capacity / drainPerSecond;
const depletedBeforeDrained = dps * timeToDrain;
if (depletedBeforeDrained >= amount) {
return amount / dps;
} else {
const restToDeplete = amount - depletedBeforeDrained;
// We delete the rest at the reduced rate
const reducedDps = dps * (recharge / eps);
return timeToDrain + (restToDeplete / reducedDps);
}
}
}

View File

@@ -47,6 +47,7 @@ export const ModuleGroupToName = {
pcm: 'First Class Passenger Cabin', pcm: 'First Class Passenger Cabin',
pcq: 'Luxury Passenger Cabin', pcq: 'Luxury Passenger Cabin',
cc: 'Collector Limpet Controller', cc: 'Collector Limpet Controller',
ss: 'Surface Scanner',
// Hard Points // Hard Points
bl: 'Beam Laser', bl: 'Beam Laser',

View File

@@ -554,10 +554,10 @@ export default class Module {
getEps() { getEps() {
// EPS is a synthetic value // EPS is a synthetic value
let distdraw = this.getDistDraw(); let distdraw = this.getDistDraw();
let rpshot = this.roundspershot || 1; // We don't use rpshot here as dist draw is per combined shot
let rof = this.getRoF() || 1; let rof = this.getRoF() || 1;
return distdraw * rpshot * rof; return distdraw * rof;
} }
/** /**
@@ -567,10 +567,10 @@ export default class Module {
getHps() { getHps() {
// HPS is a synthetic value // HPS is a synthetic value
let heat = this.getThermalLoad(); let heat = this.getThermalLoad();
let rpshot = this.roundspershot || 1; // We don't use rpshot here as dist draw is per combined shot
let rof = this.getRoF() || 1; let rof = this.getRoF() || 1;
return heat * rpshot * rof; return heat * rof;
} }
/** /**

View File

@@ -1,6 +1,7 @@
import * as Calc from './Calculations'; import * as Calc from './Calculations';
import * as ModuleUtils from './ModuleUtils'; import * as ModuleUtils from './ModuleUtils';
import * as Utils from '../utils/UtilityFunctions'; import * as Utils from '../utils/UtilityFunctions';
import { getBlueprint } from '../utils/BlueprintFunctions';
import Module from './Module'; import Module from './Module';
import LZString from 'lz-string'; import LZString from 'lz-string';
import * as _ from 'lodash'; import * as _ from 'lodash';
@@ -138,17 +139,6 @@ export default class Ship {
this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost this.standard[4].m.getEnginesCapacity() > this.boostEnergy; // PD capacitor is sufficient for boost
} }
/**
* Calculate hypothetical jump range using the installed FSD and the
* specified mass which can be more or less than ships actual mass
* @param {Number} fuel Fuel available in tons
* @param {Number} cargo Cargo in tons
* @return {Number} Jump range in Light Years
*/
calcJumpRangeWith(fuel, cargo) {
return Calc.jumpRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
}
/** /**
* Calculate the hypothetical laden jump range based on a potential change in mass, fuel, or FSD * Calculate the hypothetical laden jump range based on a potential change in mass, fuel, or FSD
* @param {Number} massDelta Optional - Change in laden mass (mass + cargo + fuel) * @param {Number} massDelta Optional - Change in laden mass (mass + cargo + fuel)
@@ -173,17 +163,6 @@ export default class Ship {
return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel);
} }
/**
* Calculate cumulative (total) jump range when making longest jumps using the installed FSD and the
* specified mass which can be more or less than ships actual mass
* @param {Number} fuel Fuel available in tons
* @param {Number} cargo Cargo in tons
* @return {Number} Total/Cumulative Jump range in Light Years
*/
calcFastestRangeWith(fuel, cargo) {
return Calc.fastestRange(this.unladenMass + fuel + cargo, this.standard[2].m, fuel);
}
/** /**
* Calculate the hypothetical top speeds at cargo and fuel tonnage * Calculate the hypothetical top speeds at cargo and fuel tonnage
* @param {Number} fuel Fuel available in tons * @param {Number} fuel Fuel available in tons
@@ -194,6 +173,54 @@ export default class Ship {
return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed); return Calc.speed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed);
} }
/**
* Calculate the speed for a given configuration
* @param {Number} eng Number of pips in ENG
* @param {Number} fuel Amount of fuel carried
* @param {Number} cargo Amount of cargo carried
* @param {boolean} boost true if boost is applied
* @return {Number} Speed
*/
calcSpeed(eng, fuel, cargo, boost) {
return Calc.calcSpeed(this.unladenMass + fuel + cargo, this.speed, this.standard[1].m, this.pipSpeed, eng, this.boost / this.speed, boost);
}
/**
* Calculate the pitch for a given configuration
* @param {Number} eng Number of pips in ENG
* @param {Number} fuel Amount of fuel carried
* @param {Number} cargo Amount of cargo carried
* @param {boolean} boost true if boost is applied
* @return {Number} Pitch
*/
calcPitch(eng, fuel, cargo, boost) {
return Calc.calcPitch(this.unladenMass + fuel + cargo, this.pitch, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
}
/**
* Calculate the roll for a given configuration
* @param {Number} eng Number of pips in ENG
* @param {Number} fuel Amount of fuel carried
* @param {Number} cargo Amount of cargo carried
* @param {boolean} boost true if boost is applied
* @return {Number} Roll
*/
calcRoll(eng, fuel, cargo, boost) {
return Calc.calcRoll(this.unladenMass + fuel + cargo, this.roll, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
}
/**
* Calculate the yaw for a given configuration
* @param {Number} eng Number of pips in ENG
* @param {Number} fuel Amount of fuel carried
* @param {Number} cargo Amount of cargo carried
* @param {boolean} boost true if boost is applied
* @return {Number} Yaw
*/
calcYaw(eng, fuel, cargo, boost) {
return Calc.calcYaw(this.unladenMass + fuel + cargo, this.yaw, this.standard[1].m, this.pipSpeed, eng, this.topBoost / this.topSpeed, boost);
}
/** /**
* Calculate the recovery time after losing or turning on shields * Calculate the recovery time after losing or turning on shields
* Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas * Thanks to CMDRs Al Gray, GIF, and Nomad Enigma for providing Shield recharge data and formulas
@@ -427,7 +454,6 @@ export default class Ship {
.recalculateDps() .recalculateDps()
.recalculateEps() .recalculateEps()
.recalculateHps() .recalculateHps()
.recalculateTtd()
.updateMovement(); .updateMovement();
} }
@@ -496,15 +522,15 @@ export default class Ship {
this.recalculateDps(); this.recalculateDps();
this.recalculateHps(); this.recalculateHps();
this.recalculateEps(); this.recalculateEps();
this.recalculateTtd();
} else if (name === 'explres' || name === 'kinres' || name === 'thermres') { } else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
m.setModValue(name, value, sentfromui); m.setModValue(name, value, sentfromui);
// Could be for shields or armour // Could be for shields or armour
this.recalculateArmour(); this.recalculateArmour();
this.recalculateShield(); this.recalculateShield();
} else if (name === 'wepcap' || name === 'weprate') { } else if (name === 'engcap') {
m.setModValue(name, value, sentfromui); m.setModValue(name, value, sentfromui);
this.recalculateTtd(); // Might have resulted in a change in boostability
this.updateMovement();
} else { } else {
// Generic // Generic
m.setModValue(name, value, sentfromui); m.setModValue(name, value, sentfromui);
@@ -563,7 +589,13 @@ export default class Ship {
this.bulkheads.m = null; this.bulkheads.m = null;
this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true);
this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {};
this.bulkheads.m.blueprint = blueprints && blueprints[0] ? blueprints[0] : {}; if (blueprints && blueprints[0]) {
this.bulkheads.m.blueprint = getBlueprint(blueprints[0].fdname, this.bulkheads.m);
this.bulkheads.m.blueprint.grade = blueprints[0].grade;
this.bulkheads.m.blueprint.special = blueprints[0].special;
} else {
this.bulkheads.m.blueprint = {};
}
this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0;
this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true;
@@ -577,7 +609,13 @@ export default class Ship {
let module = ModuleUtils.standard(i, comps.standard[i]); let module = ModuleUtils.standard(i, comps.standard[i]);
if (module != null) { if (module != null) {
module.mods = mods && mods[i + 1] ? mods[i + 1] : {}; module.mods = mods && mods[i + 1] ? mods[i + 1] : {};
module.blueprint = blueprints && blueprints[i + 1] ? blueprints[i + 1] : {}; if (blueprints && blueprints[i + 1]) {
module.blueprint = getBlueprint(blueprints[i + 1].fdname, module);
module.blueprint.grade = blueprints[i + 1].grade;
module.blueprint.special = blueprints[i + 1].special;
} else {
module.blueprint = {};
}
} }
this.use(standard[i], module, true); this.use(standard[i], module, true);
} }
@@ -599,7 +637,13 @@ export default class Ship {
let module = ModuleUtils.hardpoints(comps.hardpoints[i]); let module = ModuleUtils.hardpoints(comps.hardpoints[i]);
if (module != null) { if (module != null) {
module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; if (blueprints && blueprints[cl + i]) {
module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
module.blueprint.grade = blueprints[cl + i].grade;
module.blueprint.special = blueprints[cl + i].special;
} else {
module.blueprint = {};
}
} }
this.use(hps[i], module, true); this.use(hps[i], module, true);
} }
@@ -619,7 +663,13 @@ export default class Ship {
let module = ModuleUtils.internal(comps.internal[i]); let module = ModuleUtils.internal(comps.internal[i]);
if (module != null) { if (module != null) {
module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; module.mods = mods && mods[cl + i] ? mods[cl + i] : {};
module.blueprint = blueprints && blueprints[cl + i] ? blueprints[cl + i] : {}; if (blueprints && blueprints[cl + i]) {
module.blueprint = getBlueprint(blueprints[cl + i].fdname, module);
module.blueprint.grade = blueprints[cl + i].grade;
module.blueprint.special = blueprints[cl + i].special;
} else {
module.blueprint = {};
}
} }
this.use(internal[i], module, true); this.use(internal[i], module, true);
} }
@@ -638,7 +688,6 @@ export default class Ship {
.recalculateDps() .recalculateDps()
.recalculateEps() .recalculateEps()
.recalculateHps() .recalculateHps()
.recalculateTtd()
.updateMovement(); .updateMovement();
} }
@@ -820,7 +869,6 @@ export default class Ship {
if (slot.m.getEps()) { if (slot.m.getEps()) {
this.recalculateEps(); this.recalculateEps();
this.recalculateTtd();
} }
} }
} }
@@ -896,7 +944,6 @@ export default class Ship {
} }
if (epsChanged) { if (epsChanged) {
this.recalculateEps(); this.recalculateEps();
this.recalculateTtd();
} }
if (hpsChanged) { if (hpsChanged) {
this.recalculateHps(); this.recalculateHps();
@@ -904,9 +951,6 @@ export default class Ship {
if (powerGeneratedChange) { if (powerGeneratedChange) {
this.updatePowerGenerated(); this.updatePowerGenerated();
} }
if (powerDistributorChange) {
this.recalculateTtd();
}
if (powerUsedChange) { if (powerUsedChange) {
this.updatePowerUsed(); this.updatePowerUsed();
} }
@@ -944,33 +988,6 @@ export default class Ship {
return val; return val;
} }
/**
* Calculate time to drain WEP capacitor
* @return {this} The ship instance (for chaining operations)
*/
recalculateTtd() {
let totalSEps = 0;
for (let slotNum in this.hardpoints) {
const slot = this.hardpoints[slotNum];
if (slot.m && slot.enabled && slot.type === 'WEP' && slot.m.getDps()) {
totalSEps += slot.m.getClip() ? (slot.m.getClip() * slot.m.getEps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : slot.m.getEps();
}
}
// Calculate the drain time
const drainPerSecond = totalSEps - this.standard[4].m.getWeaponsRechargeRate();
if (drainPerSecond <= 0) {
// Can fire forever
this.timeToDrain = Infinity;
} else {
const initialCharge = this.standard[4].m.getWeaponsCapacity();
this.timeToDrain = initialCharge / drainPerSecond;
}
return this;
}
/** /**
* Calculate damage per second and related items for weapons * Calculate damage per second and related items for weapons
* @return {this} The ship instance (for chaining operations) * @return {this} The ship instance (for chaining operations)
@@ -1238,13 +1255,13 @@ export default class Ship {
shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); shield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); shieldExplRes = 1 - sgSlot.m.getExplosiveResistance();
shieldExplDRStart = shieldExplRes * 0.7; shieldExplDRStart = shieldExplRes * 0.7;
shieldExplDREnd = shieldExplRes * 0; // Currently don't know where this is shieldExplDREnd = 0;
shieldKinRes = 1 - sgSlot.m.getKineticResistance(); shieldKinRes = 1 - sgSlot.m.getKineticResistance();
shieldKinDRStart = shieldKinRes * 0.7; shieldKinDRStart = shieldKinRes * 0.7;
shieldKinDREnd = shieldKinRes * 0; // Currently don't know where this is shieldKinDREnd = 0;
shieldThermRes = 1 - sgSlot.m.getThermalResistance(); shieldThermRes = 1 - sgSlot.m.getThermalResistance();
shieldThermDRStart = shieldThermRes * 0.7; shieldThermDRStart = shieldThermRes * 0.7;
shieldThermDREnd = shieldThermRes * 0; // Currently don't know where this is shieldThermDREnd = 0;
// Shield from boosters // Shield from boosters
for (let slot of this.hardpoints) { for (let slot of this.hardpoints) {
@@ -1300,13 +1317,13 @@ export default class Ship {
let moduleprotection = 1; let moduleprotection = 1;
let hullExplRes = 1 - bulkhead.getExplosiveResistance(); let hullExplRes = 1 - bulkhead.getExplosiveResistance();
const hullExplResDRStart = hullExplRes * 0.7; 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(); let hullKinRes = 1 - bulkhead.getKineticResistance();
const hullKinResDRStart = hullKinRes * 0.7; 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(); let hullThermRes = 1 - bulkhead.getThermalResistance();
const hullThermResDRStart = hullThermRes * 0.7; 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 // Armour from HRPs and module armour from MRPs
for (let slot of this.internal) { for (let slot of this.internal) {
@@ -1346,9 +1363,9 @@ export default class Ship {
this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump
this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank
this.ladenRange = this.calcLadenRange(); // Includes full tank and caro this.ladenRange = this.calcLadenRange(); // Includes full tank and caro
this.unladenFastestRange = Calc.fastestRange(unladenMass, fsd, fuelCapacity); this.unladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity, fsd, fuelCapacity);
this.ladenFastestRange = Calc.fastestRange(unladenMass + this.cargoCapacity, fsd, fuelCapacity); this.ladenFastestRange = Calc.totalJumpRange(unladenMass + this.fuelCapacity + this.cargoCapacity, fsd, fuelCapacity);
this.maxJumpCount = Math.ceil(fuelCapacity / fsd.maxfuel); this.maxJumpCount = Math.ceil(fuelCapacity / fsd.getMaxFuelPerJump());
return this; return this;
} }

View File

@@ -5,6 +5,7 @@ const LS_KEY_BUILDS = 'builds';
const LS_KEY_COMPARISONS = 'comparisons'; const LS_KEY_COMPARISONS = 'comparisons';
const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; const LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY';
const LS_KEY_COST_TAB = 'costTab'; const LS_KEY_COST_TAB = 'costTab';
const LS_KEY_OUTFITTING_TAB = 'outfittingTab';
const LS_KEY_INSURANCE = 'insurance'; const LS_KEY_INSURANCE = 'insurance';
const LS_KEY_SHIP_DISCOUNT = 'shipDiscount'; const LS_KEY_SHIP_DISCOUNT = 'shipDiscount';
const LS_KEY_MOD_DISCOUNT = 'moduleDiscount'; const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
@@ -98,6 +99,7 @@ export class Persist extends EventEmitter {
this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {}; this.builds = buildJson && typeof buildJson == 'object' ? buildJson : {};
this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {}; this.comparisons = comparisonJson && typeof comparisonJson == 'object' ? comparisonJson : {};
this.costTab = _getString(LS_KEY_COST_TAB); this.costTab = _getString(LS_KEY_COST_TAB);
this.outfittingTab = _getString(LS_KEY_OUTFITTING_TAB);
this.state = _get(LS_KEY_STATE); this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
this.tooltipsEnabled = tips === null ? true : tips; this.tooltipsEnabled = tips === null ? true : tips;
@@ -472,6 +474,22 @@ export class Persist extends EventEmitter {
return this.costTab; return this.costTab;
} }
/**
* Persist selected outfitting tab
* @param {string} tabName Cost tab name
*/
setOutfittingTab(tabName) {
this.outfittingTab = tabName;
_put(LS_KEY_OUTFITTING_TAB, tabName);
}
/**
* Get the current outfitting tab
* @return {string} the current outfitting tab
*/
getOutfittingTab() {
return this.outfittingTab;
}
/** /**
* Retrieve the last router state from local storage * Retrieve the last router state from local storage
* @return {Object} state State object containing state name and params * @return {Object} state State object containing state name and params

View File

@@ -0,0 +1,145 @@
import React from 'react';
import { Modifications } from 'coriolis-data/dist';
/**
* Generate a tooltip with details of a blueprint's effects
* @param {Object} features The features of the blueprint
* @param {Object} m The module to compare with
* @returns {Object} The react components
*/
export function blueprintTooltip(translate, features, m)
{
const results = [];
for (const feature in features) {
const featureIsBeneficial = isBeneficial(feature, features[feature]);
const featureDef = Modifications.modifications[feature];
if (!featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let lowerBound = features[feature][0];
let upperBound = features[feature][1];
if (featureDef.type === 'percentage') {
lowerBound = Math.round(lowerBound * 1000) / 10;
upperBound = Math.round(upperBound * 1000) / 10;
}
const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
const upperIsBeneficial = isValueBeneficial(feature, upperBound);
if (m) {
// We have a module - add in the current value
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
results.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
} else {
// We do not have a module, no value
results.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
}
}
}
return (
<table>
<thead>
<tr>
<td>{translate('feature')}</td>
<td>{translate('worst')}</td>
{m ? <td>{translate('current')}</td> : null }
<td>{translate('best')}</td>
</tr>
</thead>
<tbody>
{results}
</tbody>
</table>
);
}
/**
* Is this blueprint feature beneficial?
*
*/
export function isBeneficial(feature, values) {
const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
if (Modifications.modifications[feature].higherbetter) {
return !fact;
} else {
return fact;
}
}
/**
* Is this feature value beneficial?
*
*/
export function isValueBeneficial(feature, value) {
if (Modifications.modifications[feature].higherbetter) {
return value > 0;
} else {
return value < 0;
}
}
/**
* Get a blueprint with a given name and an optional module
* @param {string} name The name of the blueprint
* @param {Object} module The module for which to obtain this blueprint
* @returns {Object} The matching blueprint
*/
export function getBlueprint(name, module) {
// Start with a copy of the blueprint
const blueprint = JSON.parse(JSON.stringify(Modifications.blueprints[name]));
if (module) {
if (module.grp === 'bh' || module.grp === 'hr') {
// Bulkheads and hull reinforcements need to have their resistances altered by the base values
for (const grade in blueprint.grades) {
for (const feature in blueprint.grades[grade].features) {
if (feature === 'explres') {
blueprint.grades[grade].features[feature][0] *= (1 - module.explres);
blueprint.grades[grade].features[feature][1] *= (1 - module.explres);
}
if (feature === 'kinres') {
blueprint.grades[grade].features[feature][0] *= (1 - module.kinres);
blueprint.grades[grade].features[feature][1] *= (1 - module.kinres);
}
if (feature === 'thermres') {
blueprint.grades[grade].features[feature][0] *= (1 - module.thermres);
blueprint.grades[grade].features[feature][1] *= (1 - module.thermres);
}
}
}
}
if (module.grp === 'sb') {
// Shield boosters are treated internally as straight modifiers, so rather than (for example)
// being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look
// accurate as per the information in Elite
for (const grade in blueprint.grades) {
for (const feature in blueprint.grades[grade].features) {
if (feature === 'shieldboost') {
blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1;
blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1)/ module.shieldboost - 1;
}
}
}
}
}
return blueprint;
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { Modifications, Modules, Ships } from 'coriolis-data/dist'; import { Modifications, Modules, Ships } from 'coriolis-data/dist';
import Module from '../shipyard/Module'; import Module from '../shipyard/Module';
import Ship from '../shipyard/Ship'; import Ship from '../shipyard/Ship';
import { getBlueprint } from '../utils/BlueprintFunctions';
// mapping from fd's ship model names to coriolis' // mapping from fd's ship model names to coriolis'
const SHIP_FD_NAME_TO_CORIOLIS_NAME = { const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
@@ -335,9 +336,9 @@ function _addModifications(module, modifiers, blueprint, grade) {
} }
} }
// Add the blueprint ID, grade and special // Add the blueprint definition, grade and special
if (blueprint) { if (blueprint) {
module.blueprint = Object.assign({}, Modifications.blueprints[blueprint]); module.blueprint = getBlueprint(blueprint, module);
if (grade) { if (grade) {
module.blueprint.grade = Number(grade); module.blueprint.grade = Number(grade);
} }
@@ -384,15 +385,16 @@ function _addModifications(module, modifiers, blueprint, grade) {
} }
// Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted. // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted.
// In addition, the modification is based off the inherent resistance of the module
if (module.grp === 'hr') { if (module.grp === 'hr') {
if (module.getModValue('explres')) { if (module.getModValue('explres')) {
module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
} }
if (module.getModValue('kinres')) { if (module.getModValue('kinres')) {
module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
} }
if (module.getModValue('thermres')) { if (module.getModValue('thermres')) {
module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
} }
} }

View File

@@ -19,6 +19,12 @@
@import 'shipselector'; @import 'shipselector';
@import 'sortable'; @import 'sortable';
@import 'loader'; @import 'loader';
@import 'pips';
@import 'boost';
@import 'movement';
@import 'shippicker';
@import 'defence';
@import 'offence';
html, body { html, body {
height: 100%; height: 100%;

14
src/less/boost.less Executable file
View File

@@ -0,0 +1,14 @@
#boost {
button {
font-size: 1.2em;
background: @primary-bg;
color: @primary;
border: 1px solid @primary;
&.selected {
// Shown when button is selected
background: @primary;
color: @primary-bg;
}
}
}

View File

@@ -44,6 +44,9 @@ svg {
.label, .text-tip { .label, .text-tip {
text-transform: capitalize; text-transform: capitalize;
}
.x {
fill: @fg; fill: @fg;
} }

14
src/less/defence.less Executable file
View File

@@ -0,0 +1,14 @@
#defence {
table {
background-color: @bgBlack;
color: @primary;
margin: 0 auto;
}
.icon {
stroke: @primary;
stroke-width: 20;
fill: transparent;
}
}

14
src/less/movement.less Normal file
View File

@@ -0,0 +1,14 @@
#movement {
svg {
width: 75%;
height: 75%;
stroke: @primary-disabled;
fill: @primary-disabled;
text {
stroke: @primary;
font-size: 2em;
}
}
}

14
src/less/offence.less Executable file
View File

@@ -0,0 +1,14 @@
#offence {
table {
background-color: @bgBlack;
color: @fg;
margin: 0 auto;
}
.icon {
stroke: @fg;
stroke-width: 20;
fill: transparent;
}
}

View File

@@ -190,9 +190,47 @@
}); });
} }
&.quarter {
width: 25%;
.tablet({
td {
line-height: 2em;
}
});
.smallTablet({
width: 50% !important;
});
}
&.third { &.third {
width: 33%; width: 33%;
.smallTablet({
width: 50% !important;
});
}
&.twothirds {
width: 67%;
.smallTablet({
width: 100% !important;
});
}
&.threequarters {
width: 75%;
.smallTablet({
width: 100% !important;
});
}
&.full {
width: 100%;
.smallTablet({ .smallTablet({
width: 100% !important; width: 100% !important;
}); });

33
src/less/pips.less Executable file
View File

@@ -0,0 +1,33 @@
// The pips table - keep the background black
#pips {
table {
background-color: @bgBlack;
color: @primary;
margin: 0 auto;
}
// A clickable entity in the pips table
.clickable {
cursor: pointer;
}
// A full pip
.full {
stroke: @primary;
fill: @primary;
}
// A half pip
.half {
stroke: @primary-disabled;
fill: @primary-disabled;
}
// An empty pip
.empty {
stroke: @primary-bg;
fill: @primary-bg;
}
}

176
src/less/shippicker.less Executable file
View File

@@ -0,0 +1,176 @@
.shippicker {
background-color: @bgBlack;
margin: 0;
height: 3em;
font-family: @fTitle;
vertical-align: middle;
position: relative;
display: block;
.user-select-none();
.menu {
position: relative;
cursor: default;
&.r {
.menu-list {
right: 0;
}
}
.smallTablet({
position: static;
position: initial;
});
}
.menu-header {
height: 100%;
z-index: 2;
padding : 0 1em;
cursor: pointer;
color: @warning;
text-transform: uppercase;
&.disabled {
color: @warning-disabled;
cursor: default;
}
&.selected {
background-color: @bgBlack;
}
.menu-item-label {
margin-left: 1em;
.smallTablet({
display: none;
});
}
}
.menu-list {
font-family: @fStandard;
position: absolute;
padding: 0.5em 1em;
box-sizing: border-box;
min-width: 100%;
overflow-x: hidden;
background-color: @bgBlack;
font-size: 0.9em;
overflow-y: auto;
z-index: 1;
-webkit-overflow-scrolling: touch;
max-height: 500px;
&::-webkit-scrollbar {
width: 0.5em;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: @warning-disabled;
}
input {
border: none;
background-color: transparent;
text-align: right;
font-size: 1em;
font-family: @fStandard;
}
.smallTablet({
max-height: 400px;
left: 0;
right: 0;
border-bottom: 1px solid @bg;
});
.tablet({
li, a {
padding: 0.3em 0;
}
});
}
.quad {
-webkit-column-count: 4; /* Chrome, Safari, Opera */
-moz-column-count: 4; /* Firefox */
column-count: 4;
ul {
min-width: 10em;
}
.smallTablet({
-webkit-column-count: 3; /* Chrome, Safari, Opera */
-moz-column-count: 3; /* Firefox */
column-count: 3;
ul {
min-width: 20em;
}
});
.largePhone({
-webkit-column-count: 2; /* Chrome, Safari, Opera */
-moz-column-count: 2; /* Firefox */
column-count: 2;
});
.smallPhone({
-webkit-column-count: 1; /* Chrome, Safari, Opera */
-moz-column-count: 1; /* Firefox */
column-count: 1;
});
}
ul {
display: inline-block;
white-space: nowrap;
margin: 0 0 0.5em;
padding: 0;
line-height: 1.3em;
color: @fg;
}
li {
white-space: normal;
list-style: none;
margin-left: 1em;
line-height: 1.1em;
color: @warning;
cursor: pointer;
&.selected {
color: @primary;
}
}
hr {
border: none;
border-top: 1px solid @disabled;
}
.no-wrap {
overflow-x: auto;
white-space: nowrap;
}
.block {
display: block;
line-height: 1.5em;
}
.title {
font-size: 1.3em;
display: inline-block;
margin:0px;
text-transform: uppercase;
}
}