Merge branch 'feature/res' into develop

This commit is contained in:
Cmdr McDonald
2016-11-21 16:39:25 +00:00
34 changed files with 2416 additions and 216 deletions

View File

@@ -6,8 +6,15 @@
* Add initial loadout passenger cabins for Beluga
* Add initial loadout passenger cabins for Orca
* Update costs and initial loadouts for Keelback and Type-7
* Add resistances for hull reinforcement packages
* Added modifier actions to create modifications from raw data
* Show modification icon for modified modules
* Take modifications in to account when deciding whether to issue a warning on a standard module
* Fix hardpoint comparison DPS number when selecting an alternate module
* Ensure that retrofit tab only shows changed modules
* Fix import and export of ships with modifications, bump schema version to 4
* Enable boost display even if power distributor is disabled
* Calculate breakdown of ship offensive and defensive stats
* Add 'Offence summary' and 'Defence summary' components
* Add ability to import from companion API output through import feature
* Add ability to import from companion API output through URL

View File

@@ -264,16 +264,28 @@
"topBoost": 248.62,
"topSpeed": 186.46,
"totalCost": 882362058,
"totalDpe": 127.26,
"totalDps": 97.74,
"totalEps": 22.71,
"totalHps": 677.29,
"totalExplDpe": 0,
"totalExplDps": 0,
"totalExplSDps": 0,
"totalHps": 33.28,
"totalKinDpe": 103.97,
"totalKinDps": 28.92,
"totalKinSDps": 21.23,
"totalSDps": 85.77,
"totalThermDpe": 23.29,
"totalThermDps": 68.82,
"totalThermSDps": 64.53,
"agility": 2,
"baseShieldStrength": 350,
"baseArmour": 945,
"hullExplRes": 0,
"hullKinRes": 0,
"hullExplRes": 0.78,
"hullKinRes": 0.73,
"hullMass": 400,
"hullThermRes": 0,
"hullThermRes": 1.37,
"masslock": 23,
"pipSpeed": 0.14,
"moduleCostMultiplier": 1,
@@ -293,8 +305,9 @@
"ladenFastestRange": 66.15,
"maxJumpCount": 4,
"shield": 833,
"shieldExplRes": 0,
"shieldKinRes": 0,
"shieldThermRes": 0
"shieldCells": 1840,
"shieldExplRes": 0.5,
"shieldKinRes": 0.6,
"shieldThermRes": 1.2
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -196,6 +196,23 @@ describe('Import Modal', function() {
});
});
describe('Import Companion API Build', function() {
beforeEach(reset);
it('imports a valid v4 build', function() {
const importData = require('./fixtures/companion-api-import-1');
pasteText(JSON.stringify(importData));
expect(modal.state.importValid).toBeTruthy();
expect(modal.state.errorMsg).toEqual(null);
expect(modal.state.singleBuild).toBe(true);
clickProceed();
expect(MockRouter.go.mock.calls.length).toBe(1);
expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette/2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifhv66g2f.AwRj4zNaKA==.CwRgDBldUExuBiQqA===.H4sIAAAAAAAAAx2Rzy4DURTGz7TuzHRu47ZjWreKlg5iQ9KFZ9CENyBWtWo8gIUFsamteAIJi0qEWIhdN11ZEN1IwyNYVKRpcXzH5su553f_XyfvKiLTYma-TkScyHVcokoYEdmbBNDsiDla-WUOT5LgyfAshHdvyGyjFFHUQCSrBU8TLT4gYq4DNL_LhNTFN3PwiqdZQyX2C-sekep-Mrs1RIbnDppsIogD1UAtN7JEM9eIzZg8hmhsEU32gFmrdgB_UARvjYEr4QMUMffoxGnV-M8X3hZ_lAO-gmWq2Eq2IVtDOzZ2Hbbuws6KxCKmKUUydgRb3woSiUXMs6Cs7Qt6FCQSi5hxkNKhj6qhfcPU_kU4wYrFMseSOmFXMKbuwZsViUWMlq1sbhvJ_lKyfqTqEJGJyoC5eIpU9x2TRnUswYXyF77BW4Z3qQuv05GDTpfvcDzvSbxJ5DtV_aHS1I4clyB2A5_b-pAL8x_enn626gEAAA==?bn=Imported%20Federal%20Corvette');
});
});
describe('Import E:D Shipyard Builds', function() {
it('imports a valid builds', function() {

View File

@@ -84,12 +84,13 @@
"dependencies": {
"babel-polyfill": "*",
"classnames": "^2.2.0",
"browserify-zlib": "ipfs/browserify-zlib",
"coriolis-data": "EDCD/coriolis-data",
"d3": "3.5.16",
"fbemitter": "^2.0.0",
"lodash": "^4.15.0",
"lz-string": "^1.4.4",
"react-number-editor": "^4.0.2",
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
"react": "^15.0.1",
"react-dom": "^15.0.1",
"superagent": "^1.4.0"

View File

@@ -7,6 +7,8 @@ import Persist from './stores/Persist';
import Header from './components/Header';
import Tooltip from './components/Tooltip';
import ModalImport from './components/ModalImport';
import * as CompanionApiUtils from './utils/CompanionApiUtils';
import { outfitURL } from './utils/UrlGenerators'
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
@@ -15,6 +17,8 @@ import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage';
import ErrorDetails from './pages/ErrorDetails';
const zlib = require('zlib');
/**
* Coriolis App
*/
@@ -52,6 +56,7 @@ export default class Coriolis extends React.Component {
this._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
this._keyDown = this._keyDown.bind(this);
this._importBuild = this._importBuild.bind(this);
this.emitter = new EventEmitter();
this.state = {
@@ -63,13 +68,36 @@ export default class Coriolis extends React.Component {
};
Router('', (r) => this._setPage(ShipyardPage, r));
Router('/import?', (r) => this._importBuild(r));
Router('/import/:data', (r) => this._importBuild(r));
Router('/outfit/?', (r) => this._setPage(OutfittingPage, r));
Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r));
Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r));
Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r));
Router('/comparison?', (r) => this._setPage(ComparisonPage, r));
Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r));
Router('/about', (r) => this._setPage(AboutPage, r));
Router('*', (r) => this._setPage(null, r));
}
/**
* Import a build directly
* @param {Object} r The current route
*/
_importBuild(r) {
try {
// Need to decode and gunzip the data, then build the ship
const data = zlib.gunzipSync(new Buffer(r.params.data, 'base64'));
const json = JSON.parse(data);
const ship = CompanionApiUtils.shipFromJson(json);
r.params.ship = ship.id;
r.params.code = ship.toString();
this._setPage(OutfittingPage, r);
} catch (err) {
this._onError('Failed to import ship', r.path, 0, 0, err);
}
}
/**
* Updates / Sets the page and route context
* @param {[type]} page The page to be shown

View File

@@ -2,6 +2,7 @@ import React from 'react';
import TranslatedComponent from './TranslatedComponent';
import Link from './Link';
import cn from 'classnames';
import { outfitURL } from '../utils/UrlGenerators';
import { SizeMap } from '../shipyard/Constants';
@@ -71,7 +72,7 @@ export default class ComparisonTable extends TranslatedComponent {
* @return {React.Component} Table row
*/
_buildRow(build, facets, formats, units) {
let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`;
let url = outfitURL(build.id, build.toString(), build.buildName)
let cells = [
<td key='s' className='tl'><Link href={url}>{build.name}</Link></td>,
<td key='bn' className='tl'><Link href={url}>{build.buildName}</Link></td>

View File

@@ -507,19 +507,19 @@ export default class CostSection extends TranslatedComponent {
scoop = true;
break;
case 'scb':
q = slotGroup[i].m.cells;
q = slotGroup[i].m.getCells();
break;
case 'am':
q = slotGroup[i].m.ammo;
q = slotGroup[i].m.getAmmo();
break;
case 'pv':
srvs += slotGroup[i].m.vehicles;
srvs += slotGroup[i].m.getBays();
break;
case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity;
break;
default:
q = slotGroup[i].m.clip + slotGroup[i].m.ammo;
q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo();
}
// Calculate ammo costs only if a cost is specified
if (slotGroup[i].m.ammocost > 0) {
@@ -532,6 +532,17 @@ export default class CostSection extends TranslatedComponent {
ammoCosts.push(item);
ammoTotal += item.total;
}
// Add fighters
if (slotGroup[i].m.grp === 'fh') {
item = {
m: slotGroup[i].m,
max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(),
cost: slotGroup[i].m.fightercost,
total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost
};
ammoCosts.push(item);
ammoTotal += item.total;
}
}
}
}
@@ -552,12 +563,13 @@ export default class CostSection extends TranslatedComponent {
item = {
m: { name: 'SRVs', class: '', rating: '' },
max: srvs,
cost: 6005,
total: srvs * 6005
cost: 1030,
total: srvs * 1030
};
ammoCosts.push(item);
ammoTotal += item.total;
}
// Calculate refuel costs if no scoop present
if (!scoop) {
item = {

View File

@@ -0,0 +1,75 @@
import React from 'react';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
/**
* Defence summary
*/
export default class DefenceSummary extends TranslatedComponent {
static PropTypes = {
ship: React.PropTypes.object.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
}
/**
* Render defence summary
* @return {React.Component} contents
*/
render() {
let ship = this.props.ship;
let { language, tooltip, termtip } = this.context;
let { formats, translate, units } = language;
let hide = tooltip.bind(null, null);
return (
<span>
<h1>{translate('defence summary')}</h1>
<table className='summary' style={{ marginLeft: 'auto', marginRight: 'auto', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody>
{ship.shield ?
<tr>
<td colSpan='4' className='summary'><h2>{translate('shields')}: {formats.int(ship.shield)} {units.MJ}</h2></td>
</tr> : null }
{ship.shield ?
<tr>
<td className='ri' onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECOVER', { cap: 0 })} onMouseLeave={hide}>{translate('recovery')}</td>
<td className='le'>{formats.time(ship.calcShieldRecovery())}</td>
<td className='ri' onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECHARGE', { cap: 0 })} onMouseLeave={hide}>{translate('recharge')}</td>
<td className='le'>{formats.time(ship.calcShieldRecharge())}</td>
</tr> : null }
{ship.shield ?
<tr>
<td className='le'>{translate('damage from')}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.pct1(ship.shieldExplRes || 1)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.pct1(ship.shieldKinRes || 1)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.pct1(ship.shieldThermRes || 1)}</td>
</tr> : null }
{ ship.shield && ship.shieldCells ?
<tr>
<td colSpan='4'><h2>{translate('shield cells')}: {formats.int(ship.shieldCells)} {units.MJ}</h2></td>
</tr> : null }
<tr>
<td colSpan='4'><h2>{translate('armour')}: {formats.int(ship.armour)}</h2></td>
</tr>
<tr>
<td className='le'>{translate('damage from')}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.pct1(ship.hullExplRes || 1)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.pct1(ship.hullKinRes || 1)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.pct1(ship.hullThermRes || 1)}</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import Slot from './Slot';
import Persist from '../stores/Persist';
import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -41,6 +42,7 @@ export default class HardpointSlot extends Slot {
let { drag, drop } = this.props;
let { termtip, tooltip } = this.context;
let validMods = Modifications.validity[m.grp] || [];
let showModuleResistances = Persist.showModuleResistances();
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
<div className={'cb'}>
@@ -62,10 +64,15 @@ 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.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.getRange() && !m.getDps() ? <div className={'l'}>{translate('Range')} : {formats.round(m.getRange() / 1000)}{u.km}</div> : null }
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null }
{ m.getShieldBoost() ? <div className={'l'}>+{formats.pct1(m.getShieldBoost())}</div> : null }
{ m.getAmmo() ? <div className={'l'}>{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}</div> : null }
{ m.getJitter() ? <div className={'l'}>{translate('jitter')}: {formats.f2(m.getJitter())}°</div> : null }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ m && validMods.length > 0 ? <div className='r' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>;
} else {

View File

@@ -203,6 +203,13 @@ export default class Header extends TranslatedComponent {
Persist.showTooltips(!Persist.showTooltips());
}
/**
* Toggle module resistances setting
*/
_toggleModuleResistances() {
Persist.showModuleResistances(!Persist.showModuleResistances());
}
/**
* Show delete all modal
* @param {SyntheticEvent} e Event
@@ -359,6 +366,7 @@ export default class Header extends TranslatedComponent {
_getSettingsMenu() {
let translate = this.context.language.translate;
let tips = Persist.showTooltips();
let moduleResistances = Persist.showModuleResistances();
return (
<div className='menu-list no-wrap cap' onClick={ (e) => e.stopPropagation() }>
@@ -376,6 +384,10 @@ export default class Header extends TranslatedComponent {
<td>{translate('tooltips')}</td>
<td className={cn('ri', { disabled: !tips, 'primary-disabled': tips })}>{(tips ? '✓' : '✗')}</td>
</tr>
<tr className='cap ptr' onClick={this._toggleModuleResistances} >
<td>{translate('module resistances')}</td>
<td className={cn('ri', { disabled: !moduleResistances, 'primary-disabled': moduleResistances })}>{(moduleResistances ? '✓' : '✗')}</td>
</tr>
<tr>
<td>{translate('insurance')}</td>
<td className='ri'>
@@ -438,6 +450,7 @@ export default class Header extends TranslatedComponent {
Persist.addListener('deletedAll', update);
Persist.addListener('builds', update);
Persist.addListener('tooltips', update);
Persist.addListener('moduleresistances', update);
}
/**

View File

@@ -1,5 +1,6 @@
import React from 'react';
import Slot from './Slot';
import Persist from '../stores/Persist';
import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
@@ -23,6 +24,7 @@ export default class InternalSlot extends Slot {
let { drag, drop, ship } = this.props;
let { termtip, tooltip } = this.context;
let validMods = Modifications.validity[m.grp] || [];
let showModuleResistances = Persist.showModuleResistances();
let mass = m.getMass() || m.cargo || m.fuel || 0;
return <div className='details' draggable='true' onDragStart={drag} onDragEnd={drop}>
@@ -38,7 +40,7 @@ export default class InternalSlot extends Slot {
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
{ m.getAmmo() ? <div className={'l'}>{translate('ammunition')}: {formats.gen(m.getAmmo())}</div> : null }
{ m.cells ? <div className={'l'}>{translate('cells')}: {m.cells}</div> : null }
{ m.recharge ? <div className={'l'}>{translate('recharge')}: {m.recharge} <u>MJ</u>&nbsp;&nbsp;&nbsp;{translate('total')}: {m.cells * m.recharge}{u.MJ}</div> : null }
{ m.shieldreinforcement ? <div className={'l'}>{translate('shieldreinforcement')}: {formats.int(m.getShieldReinforcement())} <u>MJ</u>&nbsp;&nbsp;&nbsp;{translate('total')}: {formats.int(m.cells * m.getShieldReinforcement())}{u.MJ}</div> : null }
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
{ m.getFacingLimit() ? <div className={'l'}>{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°</div> : null }
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f2(m.getRange())}{u.km}</div> : null }
@@ -49,8 +51,12 @@ export default class InternalSlot extends Slot {
{ m.rangeLS ? <div className={'l'}>{translate('range')}: {m.rangeLS}{u.Ls}</div> : null }
{ m.rangeLS === null ? <div className={'l'}>{u.Ls}</div> : null }
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
{ m.getHullReinforcement() ? <div className={'l'}>+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost'))} <u className='cap'>{translate('armour')}</u></div> : null }
{ m.getHullReinforcement() ? <div className={'l'}>+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} <u className='cap'>{translate('armour')}</u></div> : null }
{ m.passengers ? <div className={'l'}>{translate('passengers')}: {m.passengers}</div> : null }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ m && validMods.length > 0 ? <div className='r' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>

View File

@@ -11,6 +11,7 @@ import * as ModuleUtils from '../shipyard/ModuleUtils';
import { fromDetailedBuild } from '../shipyard/Serializer';
import { Download } from './SvgIcons';
import { outfitURL } from '../utils/UrlGenerators';
import * as CompanionApiUtils from '../utils/CompanionApiUtils';
const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n');
const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)');
@@ -112,6 +113,7 @@ export default class ModalImport extends TranslatedComponent {
this._importBackup = this._importBackup.bind(this);
this._importDetailedArray = this._importDetailedArray.bind(this);
this._importTextBuild = this._importTextBuild.bind(this);
this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this);
this._validateImport = this._validateImport.bind(this);
}
@@ -183,6 +185,21 @@ export default class ModalImport extends TranslatedComponent {
this.setState({ builds });
}
/**
* Import a build direct from the companion API
* @param {string} build JSON from the companion API information
* @throws {string} if parse/import fails
*/
_importCompanionApiBuild(build) {
const shipModel = CompanionApiUtils.shipModelFromJson(build);
const ship = CompanionApiUtils.shipFromJson(build);
let builds = {};
builds[shipModel] = {};
builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString();
this.setState({ builds, singleBuild: true });
}
/**
* Import a text build from ED Shipyard
* @param {string} buildStr Build string
@@ -315,7 +332,11 @@ export default class ModalImport extends TranslatedComponent {
throw 'Must be an object or array!';
}
if (importData instanceof Array) { // Must be detailed export json
if (importData.modules != null && importData.modules.Armour != null) { // Only the companion API has this information
this._importCompanionApiBuild(importData); // Single sihp definition
} else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != null) { // Only the companion API has this information
this._importCompanionApiBuild(importData.ship); // Complete API dump
} else if (importData instanceof Array) { // Must be detailed export json
this._importDetailedArray(importData);
} else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export
this._importDetailedArray([importData]); // Convert to array with singleobject

View File

@@ -7,7 +7,7 @@ import NumberEditor from 'react-number-editor';
/**
* Modification
*/
export default class ModificationsMenu extends TranslatedComponent {
export default class Modification extends TranslatedComponent {
static propTypes = {
ship: React.PropTypes.object.isRequired,
@@ -24,26 +24,30 @@ export default class ModificationsMenu extends TranslatedComponent {
constructor(props, context) {
super(props);
this.state = {};
this.state.value = this.props.m.getModValue(this.props.name) * 100 || 0;
this.state.value = this.props.m.getModValue(this.props.name) / 100 || 0;
}
/**
* Update modification given a value.
* @param {Number} value The value to set
* @param {Number} value The value to set. This comes in as a string and must be stored in state as a string,
* because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing
* in a value by hand
*/
_updateValue(value) {
let scaledValue = Math.floor(Number(value) * 100) / 10000;
const name = this.props.name;
let scaledValue = Math.round(Number(value) * 100);
// Limit to +1000% / -100%
if (scaledValue > 10) {
scaledValue = 10;
if (scaledValue > 100000) {
scaledValue = 100000;
value = 1000;
}
if (scaledValue < -1) {
scaledValue = -1;
if (scaledValue < -10000) {
scaledValue = -10000;
value = -100;
}
let m = this.props.m;
let name = this.props.name;
let ship = this.props.ship;
ship.setModification(m, name, scaledValue);
@@ -62,7 +66,7 @@ export default class ModificationsMenu extends TranslatedComponent {
return (
<div className={'cb'} key={name}>
<div className={'cb'}>{translate(name)}{name === 'jitter' ? ' (°)' : ' (%)'}</div>
<NumberEditor className={'cb'} style={{ width: '100%', textAlign: 'center' }} step={0.01} stepModifier={1} decimals={2} value={this.state.value} onValueChange={this._updateValue.bind(this)} />
<NumberEditor className={'cb'} style={{ width: '90%', textAlign: 'center' }} step={0.01} stepModifier={1} decimals={2} value={this.state.value} onValueChange={this._updateValue.bind(this)} />
</div>
);
}

View File

@@ -50,11 +50,13 @@ export default class ModificationsMenu extends TranslatedComponent {
* @return {React.Component} List
*/
render() {
let { tooltip, termtip } = this.context;
return (
<div
className={cn('select', this.props.className)}
onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation}
onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)}
>
{this.state.list}
</div>

View File

@@ -0,0 +1,70 @@
import React from 'react';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons';
/**
* Offence summary
*/
export default class OffenceSummary extends TranslatedComponent {
static PropTypes = {
ship: React.PropTypes.object.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
}
/**
* Render offence summary
* @return {React.Component} contents
*/
render() {
let ship = this.props.ship;
let { language, tooltip, termtip } = this.context;
let { formats, translate } = language;
return (
<span>
<h1>{translate('offence summary')}</h1>
<table className='summary' style={{ marginLeft: 'auto', marginRight: 'auto', lineHeight: '1em', backgroundColor: 'transparent' }}>
<tbody>
<tr>
<td colSpan='4' className='summary'><h2>{translate('dps')}: {formats.f1(ship.totalDps)}</h2></td>
</tr>
<tr>
<td className='le'>{translate('damage by')}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplDps)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinDps)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermDps)}</td>
</tr>
<tr>
<td colSpan='4' className='summary'><h2>{translate('sdps')}: {formats.f1(ship.totalSDps)}</h2></td>
</tr>
<tr>
<td className='le'>{translate('damage by')}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplSDps)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinSDps)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermSDps)}</td>
</tr>
<tr>
<td colSpan='4' className='summary'><h2>{translate('dpe')}: {formats.f1(ship.totalDpe)}</h2></td>
</tr>
<tr>
<td className='le'>{translate('damage by')}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'explosive')} onMouseOut={tooltip.bind(null, null)}><DamageExplosive /> {formats.f1(ship.totalExplDpe)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'kinetic')} onMouseOut={tooltip.bind(null, null)}><DamageKinetic /> {formats.f1(ship.totalKinDpe)}</td>
<td className='ri' onMouseOver={termtip.bind(null, 'thermal')} onMouseOut={tooltip.bind(null, null)}><DamageThermal /> {formats.f1(ship.totalThermDpe)}</td>
</tr>
</tbody>
</table>
</span>
);
}
}

View File

@@ -34,20 +34,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
sgRecharge = time(ship.calcShieldRecharge());
}
// <th colSpan={3}>{translate('shield resistance')}</th>
// <th colSpan={3}>{translate('hull resistance')}</th>
// <th className='lft'>{translate('explosive')}</th>
// <th className='lft'>{translate('kinetic')}</th>
// <th className='lft'>{translate('thermal')}</th>
// <th className='lft'>{translate('explosive')}</th>
// <th className='lft'>{translate('kinetic')}</th>
// <th className='lft'>{translate('thermal')}</th>
// <td>{pct(ship.shieldExplRes)}</td>
// <td>{pct(ship.shieldKinRes)}</td>
// <td>{pct(ship.shieldThermRes)}</td>
// <td>{pct(ship.hullExplRes)}</td>
// <td>{pct(ship.hullKinRes)}</td>
// <td>{pct(ship.hullThermRes)}</td>
return <div id='summary'>
<table id='summaryTable'>
<thead>
@@ -58,7 +44,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
<th onMouseEnter={termtip.bind(null, 'energy per second')} onMouseLeave={hide} rowSpan={2}>{translate('EPS')}</th>
<th onMouseEnter={termtip.bind(null, 'heat per second')} onMouseLeave={hide} rowSpan={2}>{translate('HPS')}</th>
<th rowSpan={2}>{translate('armour')}</th>
<th colSpan={3}>{translate('shields')}</th>
<th rowSpan={2}>{translate('shields')}</th>
<th colSpan={3}>{translate('mass')}</th>
<th rowSpan={2}>{translate('cargo')}</th>
<th rowSpan={2}>{translate('fuel')}</th>
@@ -67,9 +53,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
<th onMouseEnter={termtip.bind(null, 'mass lock factor')} onMouseLeave={hide} rowSpan={2}>{translate('MLF')}</th>
</tr>
<tr>
<th className='lft'>{translate('strength')}</th>
<th onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECOVER', { cap: 0 })} onMouseLeave={hide}>{translate('recovery')}</th>
<th onMouseEnter={termtip.bind(null, 'PHRASE_SG_RECHARGE', { cap: 0 })} onMouseLeave={hide}>{translate('recharge')}</th>
<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>
@@ -90,8 +73,6 @@ export default class ShipSummaryTable extends TranslatedComponent {
<td>{f1(ship.totalHps)}</td>
<td>{int(ship.armour)}</td>
<td className={sgClassNames}>{int(ship.shield)} {u.MJ}</td>
<td className={sgClassNames}>{sgRecover}</td>
<td className={sgClassNames}>{sgRecharge}</td>
<td>{ship.hullMass} {u.T}</td>
<td>{int(ship.unladenMass)} {u.T}</td>
<td>{int(ship.ladenMass)} {u.T}</td>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import cn from 'classnames';
import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent';
import { jumpRange } from '../shipyard/Calculations';
import { diffDetails } from '../utils/SlotFunctions';
@@ -46,6 +47,7 @@ export default class StandardSlot extends TranslatedComponent {
let classRating = m.class + m.rating;
let menu;
let validMods = m == null ? [] : (Modifications.validity[m.grp] || []);
let showModuleResistances = Persist.showModuleResistances();
let mass = m.getMass() || m.cargo || m.fuel || 0;
if (!selected) {
@@ -79,11 +81,10 @@ export default class StandardSlot extends TranslatedComponent {
<div className={cn('details-container', { warning: warning && warning(slot.m) })}>
<div className={'sz'}>{slot.maxClass}</div>
<div>
<div className={'l'}>{classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}{ m.mods && Object.keys(m.mods).length > 0 ? <span className='r' onMouseOver={termtip.bind(null, 'modified')} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : null }</div>
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r' onMouseOver={termtip.bind(null, 'modified')} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : null }</div>
<div className={'r'}>{formats.round(mass)}{units.T}</div>
<div/>
<div className={'cb'}>
{ m.grp == 'bh' && m.name ? <div className='l'>{translate(m.name)}</div> : null }
{ m.getOptimalMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptimalMass())}{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 }
@@ -94,6 +95,9 @@ export default class StandardSlot extends TranslatedComponent {
{ m.getWeaponsCapacity() ? <div className='l'>{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}</div> : null }
{ m.getSystemsCapacity() ? <div className='l'>{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}</div> : null }
{ m.getEnginesCapacity() ? <div className='l'>{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}</div> : null }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ validMods.length > 0 ? <div className='r' ><button onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>

View File

@@ -4,7 +4,6 @@ import SlotSection from './SlotSection';
import StandardSlot from './StandardSlot';
import Module from '../shipyard/Module';
import { diffDetails } from '../utils/SlotFunctions';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import * as ShipRoles from '../shipyard/ShipRoles';
import { stopCtxPropagation } from '../utils/UtilityFunctions';

View File

@@ -29,6 +29,8 @@ export const terms = {
PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo',
PHRASE_UPDATE_RDY: 'Update Available! Click to refresh',
HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes',
// Other languages fallback to these values
// Only Translate to other languages if the name is different in-game
am: 'Auto Field-Maintenance Unit',
@@ -92,14 +94,18 @@ export const terms = {
// Unit for seconds
secs: 's',
// Hardpoint abbreviations
// Weapon, offence and defence
dpe: 'Damage per MJ of energy',
dps: 'Damage per second',
sdps: 'Sustained damage per second',
dpssdps: 'Damage per second (sustained damage per second)',
eps: 'Energy per second',
epsseps: 'Energy per second (sustained energy per second)',
hps: 'Heat per second',
hpsshps: 'Heat per second (sustained heat per second)',
'damage by': 'Damage by',
'damage from': 'Damage from',
'shield cells': 'Shield cells',
// Modifications
ammo: 'Ammunition maximum',
@@ -134,6 +140,7 @@ export const terms = {
rof: 'Rate of fire',
shield: 'Shield',
shieldboost: 'Shield boost',
shieldreinforcement: 'Shield reinforcement',
spinup: 'Spin up time',
syscap: 'Systems capacity',
sysrate: 'Systems recharge rate',

View File

@@ -345,7 +345,7 @@ export default class ComparisonPage extends Page {
let code = fromComparison(name, builds, selectedFacets, predicate, desc);
let loc = window.location;
return `${loc.protocol}//${loc.host}/comparison/${code}`;
return loc.protocol + '//' + loc.host + '/comparison?code=' + encodeURIComponent(code);
}
/**

View File

@@ -26,7 +26,7 @@ export default class ErrorDetails extends React.Component {
if (ed) {
content = <div style={{ textAlign:'left', fontSize:'0.8em', width: '43em', margin: '0 auto' }}>
<div className='cen'>
<a href='https://github.com/cmmcleod/coriolis/issues' target='_blank' title='Coriolis Github Project'>Create an issue on Github</a>
<a href='https://github.com/edcd/coriolis/issues' target='_blank' title='Coriolis Github Project'>Create an issue on Github</a>
{' if this keeps happening. Add these details:'}
</div>
<div style={{ marginTop: '2em' }}>

View File

@@ -8,13 +8,14 @@ import Persist from '../stores/Persist';
import Ship from '../shipyard/Ship';
import { toDetailedBuild } from '../shipyard/Serializer';
import { outfitURL } from '../utils/UrlGenerators';
import { FloppyDisk, Bin, Switch, Download, Reload, Fuel } from '../components/SvgIcons';
import ShipSummaryTable from '../components/ShipSummaryTable';
import StandardSlotSection from '../components/StandardSlotSection';
import HardpointsSlotSection from '../components/HardpointsSlotSection';
import InternalSlotSection from '../components/InternalSlotSection';
import UtilitySlotSection from '../components/UtilitySlotSection';
import OffenceSummary from '../components/OffenceSummary';
import DefenceSummary from '../components/DefenceSummary';
import LineChart from '../components/LineChart';
import PowerManagement from '../components/PowerManagement';
import CostSection from '../components/CostSection';
@@ -288,12 +289,14 @@ export default class OutfittingPage extends Page {
sStr = ship.getStandardString() + '.' + ship.getModificationsString(),
iStr = ship.getInternalString() + '.' + ship.getModificationsString();
Router.replace(outfitURL(ship.id, code, buildName));
return (
<div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}>
<div id='overview'>
<h1>{ship.name}</h1>
<div id='build'>
<input value={newBuildName} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxLength={50} />
<input value={newBuildName || ''} onChange={this._buildNameChange} placeholder={translate('Enter Name')} maxLength={50} />
<button onClick={canSave && this._saveBuild} disabled={!canSave} onMouseOver={termtip.bind(null, 'save')} onMouseOut={hide}>
<FloppyDisk className='lg' />
</button>
@@ -324,31 +327,11 @@ export default class OutfittingPage extends Page {
<CostSection ship={ship} buildName={buildName} code={sStr + hStr + iStr} />
<div ref='chartThird' className='group third'>
<h1>{translate('jump range')}</h1>
<LineChart
width={chartWidth}
xMax={ship.cargoCapacity}
yMax={ship.unladenRange}
xUnit={translate('T')}
yUnit={translate('LY')}
yLabel={translate('jump range')}
xLabel={translate('cargo')}
func={state.jumpRangeChartFunc}
/>
<OffenceSummary ship={ship} code={code}/>
</div>
<div className='group third'>
<h1>{translate('total range')}</h1>
<LineChart
width={chartWidth}
xMax={ship.cargoCapacity}
yMax={ship.unladenFastestRange}
xUnit={translate('T')}
yUnit={translate('LY')}
yLabel={translate('fastest range')}
xLabel={translate('cargo')}
func={state.fastestRangeChartFunc}
/>
<DefenceSummary ship={ship} code={code}/>
</div>
<div className='group third'>
@@ -397,3 +380,16 @@ export default class OutfittingPage extends Page {
);
}
}
// <div ref='chartThird' className='group third'>
// <h1>{translate('jump range')}</h1>
// <LineChart
// width={chartWidth}
// xMax={ship.cargoCapacity}
// yMax={ship.unladenRange}
// xUnit={translate('T')}
// yUnit={translate('LY')}
// yLabel={translate('jump range')}
// xLabel={translate('cargo')}
// func={state.jumpRangeChartFunc}
// />
// </div>

View File

@@ -55,6 +55,7 @@ function shipSummary(shipId, shipData) {
ship.optimizeMass({ th: ship.standard[1].maxClass + 'A', fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters
summary.topSpeed = ship.topSpeed;
summary.topBoost = ship.topBoost;
summary.baseArmour = ship.armour;
return summary;
}
@@ -141,7 +142,7 @@ export default class ShipyardPage extends Page {
<td>{s.agility}</td>
<td className='ri'>{fInt(s.speed)}{u['m/s']}</td>
<td className='ri'>{fInt(s.boost)}{u['m/s']}</td>
<td className='ri'>{s.baseArmour}</td>
<td className='ri'>{fInt(s.baseArmour)}</td>
<td className='ri'>{fInt(s.baseShieldStrength)}{u.MJ}</td>
<td className='ri'>{fInt(s.topSpeed)}{u['m/s']}</td>
<td className='ri'>{fInt(s.topBoost)}{u['m/s']}</td>

View File

@@ -125,7 +125,7 @@ export const ShipFacets = [
},
{ // 3
title: 'shields',
props: ['shieldStrength'],
props: ['shield'],
unit: 'MJ',
fmt: 'int',
i: 3

View File

@@ -28,16 +28,16 @@ export default class Module {
/**
* Get a value for a given modification
* @param {Number} name The name of the modification
* @return {Number} The value of the modification, as a decimal value where 1 is 100%
* @return {Number} The value of the modification, as an integer value scaled so that 1.23% == 123
*/
getModValue(name) {
return this.mods && this.mods[name] ? this.mods[name] / 10000 : null;
return this.mods && this.mods[name] ? this.mods[name] : null;
}
/**
* Set a value for a given modification ID
* @param {Number} name The name of the modification
* @param {Number} value The value of the modification, as a decimal value where 1 is 100%
* @param {Number} value The value of the modification, as an integer scaled so that -2.34% == -234
*/
setModValue(name, value) {
if (!this.mods) {
@@ -47,27 +47,41 @@ export default class Module {
if (value == null || value == 0) {
delete this.mods[name];
} else {
// Store value with 2dp
this.mods[name] = Math.round(value * 10000);
// Round just to be sure
this.mods[name] = Math.round(value);
}
}
/**
* Helper to obtain a modified value using standard multipliers
* @param {String} name the name of the modifier to obtain
* @return {Number} the mass of this module
* @param {String} name the name of the modifier to obtain
* @param {Boolean} additive Optional true if the value is additive rather than multiplicative
* @return {Number} the mass of this module
*/
_getModifiedValue(name) {
let result = 0;
if (this[name]) {
result = this[name];
if (result) {
let mult = this.getModValue(name);
if (mult) { result = result * (1 + mult); }
_getModifiedValue(name, additive) {
let result = this[name] || (additive ? 0 : null); // Additive NULL === 0
if (result != null) {
// Jitter is special, being the only non-percentage value (it is in fact degrees)
const modValue = name === 'jitter' ? this.getModValue(name) / 100 : this.getModValue(name) / 10000;
if (modValue) {
if (additive) {
result = result + modValue;
} else {
result = result * (1 + modValue);
}
}
}
return result;
}
/**
* Return true if this is a shield generator
* @return {Boolean} if this is a shield generator
*/
isShieldGenerator() {
return (this.grp === 'sg' || this.grp === 'psg' || this.grp === 'bsg');
}
/**
* Get the power generation of this module, taking in to account modifications
* @return {Number} the power generation of this module
@@ -193,7 +207,7 @@ export default class Module {
* @return {Number} the kinetic resistance of this module
*/
getKineticResistance() {
return this._getModifiedValue('kinres');
return this._getModifiedValue('kinres', true);
}
/**
@@ -201,7 +215,7 @@ export default class Module {
* @return {Number} the thermal resistance of this module
*/
getThermalResistance() {
return this._getModifiedValue('thermres');
return this._getModifiedValue('thermres', true);
}
/**
@@ -209,7 +223,7 @@ export default class Module {
* @return {Number} the explosive resistance of this module
*/
getExplosiveResistance() {
return this._getModifiedValue('explres');
return this._getModifiedValue('explres', true);
}
/**
@@ -294,7 +308,7 @@ export default class Module {
if (this['minmass']) {
result = this['minmass'];
if (result) {
let mult = this.getModValue('optmass');
let mult = this.getModValue('optmass') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -319,7 +333,7 @@ export default class Module {
if (this['maxmass']) {
result = this['maxmass'];
if (result) {
let mult = this.getModValue('optmass');
let mult = this.getModValue('optmass') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -336,7 +350,7 @@ export default class Module {
if (this['minmul']) {
result = this['minmul'];
if (result) {
let mult = this.getModValue('optmul');
let mult = this.getModValue('optmul') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -361,7 +375,7 @@ export default class Module {
if (this['maxmul']) {
result = this['maxmul'];
if (result) {
let mult = this.getModValue('optmul');
let mult = this.getModValue('optmul') / 10000;
if (mult) { result = result * (1 + mult); }
}
}
@@ -487,4 +501,44 @@ export default class Module {
return this._getModifiedValue('hullboost');
}
/**
* Get the shield reinforcement for this module, taking in to account modifications
* @return {Number} the shield reinforcement for this module
*/
getShieldReinforcement() {
return this._getModifiedValue('shieldreinforcement');
}
/**
* Get the bays for this module, taking in to account modifications
* @return {Number} the bays for this module
*/
getBays() {
return this._getModifiedValue('bays');
}
/**
* Get the rebuilds per bay for this module, taking in to account modifications
* @return {Number} the rebuilds per bay for this module
*/
getRebuildsPerBay() {
return this._getModifiedValue('rebuildsperbay');
}
/**
* Get the cells for this module, taking in to account modifications
* @return {Number} the cells for this module
*/
getCells() {
return this._getModifiedValue('cells');
}
/**
* Get the jitter for this module, taking in to account modifications
* @return {Number} the jitter for this module
*/
getJitter() {
return this._getModifiedValue('jitter', true);
}
}

View File

@@ -2,7 +2,9 @@ import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants';
import { Ships } from 'coriolis-data/dist';
import Ship from './Ship';
import * as ModuleUtils from './ModuleUtils';
import * as Utils from '../utils/UtilityFunctions';
import LZString from 'lz-string';
import { outfitURL } from '../utils/UrlGenerators';
const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'];
@@ -83,7 +85,7 @@ export function toDetailedBuild(buildName, ship) {
ship: ship.name,
references: [{
name: 'Coriolis.io',
url: `https://coriolis.edcd.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`,
url: 'https://coriolis.edcd.io' + outfitURL(ship.id, code, buildName),
code,
shipId: ship.id
}],
@@ -215,7 +217,7 @@ export function fromComparison(name, builds, facets, predicate, desc) {
f: facets,
p: predicate,
d: desc ? 1 : 0
})).replace(/\//g, '-');
}));
};
/**
@@ -224,5 +226,5 @@ export function fromComparison(name, builds, facets, predicate, desc) {
* @return {Object} Comparison data object
*/
export function toComparison(code) {
return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/')));
return JSON.parse(LZString.decompressFromBase64(Utils.fromUrlSafe(code)));
};

View File

@@ -1,5 +1,6 @@
import * as Calc from './Calculations';
import * as ModuleUtils from './ModuleUtils';
import * as Utils from '../utils/UtilityFunctions';
import Module from './Module';
import LZString from 'lz-string';
import isEqual from 'lodash/lang';
@@ -126,7 +127,6 @@ export default class Ship {
*/
canBoost() {
return this.canThrust() && // Thrusters operational
this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational
this.boostEnergy <= this.standard[4].m.getEnginesCapacity(); // PD capacitor is sufficient for boost
}
@@ -194,10 +194,12 @@ export default class Ship {
*/
calcShieldRecovery() {
if (this.shield > 0) {
let sgSlot = this.findInternalByGroup('sg');
let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen');
// 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts
return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15;
const sgSlot = this.findInternalByGroup('sg');
if (sgSlot != null) {
let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen') / 10000;
// 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts
return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15;
}
}
return 0;
}
@@ -210,10 +212,12 @@ export default class Ship {
*/
calcShieldRecharge() {
if (this.shield > 0) {
let sgSlot = this.findInternalByGroup('sg');
let regenRate = 1 + sgSlot.m.getModValue('regen');
// 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s
return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate);
const sgSlot = this.findInternalByGroup('sg');
if (sgSlot != null) {
let regenRate = 1 + sgSlot.m.getModValue('regen') / 10000;
// 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s
return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate);
}
}
return 0;
}
@@ -233,9 +237,8 @@ export default class Ship {
sg = sgSlot.m;
}
// TODO obtain shield boost
// return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, this.shieldMultiplier + (multiplierDelta || 0));
return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 0 + (multiplierDelta || 0));
// TODO Not accurate if the ship has modified shield boosters
return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0));
}
/**
@@ -397,19 +400,24 @@ export default class Ship {
* Set a modification value
* @param {Object} m The module to change
* @param {Object} name The name of the modification to change
* @param {Number} value The new value of the modification
* @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123
*/
setModification(m, name, value) {
if (isNaN(value)) {
// Value passed is invalid; reset it to 0
value = 0;
}
// Handle special cases
if (name == 'pgen') {
if (name === 'pgen') {
// Power generation
m.setModValue(name, value);
this.updatePowerGenerated();
} else if (name == 'power') {
} else if (name === 'power') {
// Power usage
m.setModValue(name, value);
this.updatePowerUsed();
} else if (name == 'mass') {
} else if (name === 'mass') {
// Mass
let oldMass = m.getMass();
m.setModValue(name, value);
@@ -418,32 +426,40 @@ export default class Ship {
this.ladenMass = this.ladenMass - oldMass + newMass;
this.updateTopSpeed();
this.updateJumpStats();
} else if (name == 'maxfuel') {
} else if (name === 'maxfuel') {
m.setModValue(name, value);
this.updateJumpStats();
} else if (name == 'optmass') {
} else if (name === 'optmass') {
m.setModValue(name, value);
// Could be for any of thrusters, FSD or shield
this.updateTopSpeed();
this.updateJumpStats();
this.updateShield();
} else if (name == 'optmul') {
this.recalculateShield();
} else if (name === 'optmul') {
m.setModValue(name, value);
// Could be for any of thrusters, FSD or shield
this.updateTopSpeed();
this.updateJumpStats();
this.updateShield();
} else if (name == 'shieldboost') {
this.recalculateShield();
} else if (name === 'shieldboost') {
m.setModValue(name, value);
this.updateShield();
} else if (name == 'hullboost') {
this.recalculateShield();
} else if (name === 'hullboost' || name === 'hullreinforcement') {
m.setModValue(name, value);
this.updateArmour();
} else if (name == 'burst' || name == 'clip' || name == 'damage' || name == 'distdraw' || name == 'jitter' || name == 'piercing' || name == 'range' || name == 'reload' || name == 'rof' || name == 'thermload') {
this.recalculateArmour();
} else if (name === 'shieldreinforcement') {
m.setModValue(name, value);
this.recalculateShieldCells();
} else if (name === 'burst' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') {
m.setModValue(name, value);
this.recalculateDps();
this.recalculateHps();
this.recalculateEps();
} else if (name === 'explres' || name === 'kinres' || name === 'thermres') {
m.setModValue(name, value);
// Could be for shields or armour
this.recalculateArmour();
this.recalculateShield();
} else {
// Generic
m.setModValue(name, value);
@@ -471,9 +487,21 @@ export default class Ship {
this.ladenMass = 0;
this.armour = this.baseArmour;
this.shield = this.baseShieldStrength;
this.shieldCells = 0;
this.totalCost = this.m.incCost ? this.m.discountedCost : 0;
this.unladenMass = this.hullMass;
this.totalDpe = 0;
this.totalExplDpe = 0;
this.totalKinDpe = 0;
this.totalThermDpe = 0;
this.totalDps = 0;
this.totalExplDps = 0;
this.totalKinDps = 0;
this.totalThermDps = 0;
this.totalSDps = 0;
this.totalExplSDps = 0;
this.totalKinSDps = 0;
this.totalThermSDps = 0;
this.totalEps = 0;
this.totalHps = 0;
this.shieldExplRes = 0;
@@ -544,8 +572,12 @@ export default class Ship {
this.updatePowerGenerated()
.updatePowerUsed()
.updateJumpStats()
.updateShield()
.updateArmour()
.recalculateShield()
.recalculateShieldCells()
.recalculateArmour()
.recalculateDps()
.recalculateEps()
.recalculateHps()
.updateTopSpeed();
}
@@ -570,20 +602,20 @@ export default class Ship {
code = parts[0];
if (parts[1]) {
enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split('');
enabled = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[1])).split('');
}
if (parts[2]) {
priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split('');
priorities = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[2])).split('');
}
if (parts[3]) {
const modstr = parts[3].replace(/-/g, '/');
const modstr = parts[3];
if (modstr.match(':')) {
this.decodeModificationsString(modstr, modifications);
} else {
try {
this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(modstr, 'base64')), modifications);
this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(Utils.fromUrlSafe(modstr), 'base64')), modifications);
} catch (err) {
// Could be out-of-date URL; ignore
}
@@ -687,21 +719,27 @@ export default class Ship {
if (slot.enabled != enabled) { // Enabled state is changing
slot.enabled = enabled;
if (slot.m) {
if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp == 'sb') {
this.updateShield();
if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp === 'sb') {
this.recalculateShield();
}
if (slot.m.getDps()) {
this.totalDps += slot.m.getDps() * (enabled ? 1 : -1);
}
if (slot.m.getEps()) {
this.totalEps += slot.m.getEps() * (enabled ? 1 : -1);
}
if (slot.m.getHps()) {
this.totalHps += slot.m.getHps() * (enabled ? 1 : -1);
if (slot.m.grp === 'scb') {
this.recalculateShieldCells();
}
this.updatePowerUsed();
this.updatePowerEnabledString();
if (slot.m.getDps()) {
this.recalculateDps();
}
if (slot.m.getHps()) {
this.recalculateHps();
}
if (slot.m.getEps()) {
this.recalculateEps();
}
}
}
return this;
@@ -738,10 +776,15 @@ export default class Ship {
updateStats(slot, n, old, preventUpdate) {
let powerGeneratedChange = slot == this.standard[0];
let powerUsedChange = false;
let dpsChanged = n && n.getDps() || old && old.getDps();
let epsChanged = n && n.getEps() || old && old.getEps();
let hpsChanged = n && n.getHps() || old && old.getHps();
let armourChange = (slot == this.bulkheads) || (n && n.grp == 'hr') || (old && old.grp == 'hr');
let armourChange = (slot === this.bulkheads) || (n && n.grp === 'hr') || (old && old.grp === 'hr');
let shieldChange = (n && n.grp == 'bsg') || (old && old.grp == 'bsg') || (n && n.grp == 'psg') || (old && old.grp == 'psg') || (n && n.grp == 'sg') || (old && old.grp == 'sg') || (n && n.grp == 'sb') || (old && old.grp == 'sb');
let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb');
let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb');
if (old) { // Old modul now being removed
switch (old.grp) {
@@ -761,16 +804,6 @@ export default class Ship {
powerUsedChange = true;
}
if (old.getDps()) {
this.totalDps -= old.getDps();
}
if (old.getEps()) {
this.totalEps -= old.getEps();
}
if (old.getHps()) {
this.totalHps -= old.getHps();
}
this.unladenMass -= old.getMass() || 0;
}
@@ -792,22 +825,21 @@ export default class Ship {
powerUsedChange = true;
}
if (n.getDps()) {
this.totalDps += n.getDps();
}
if (n.getEps()) {
this.totalEps += n.getEps();
}
if (n.getHps()) {
this.totalHps += n.getHps();
}
this.unladenMass += n.getMass() || 0;
}
this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity;
if (!preventUpdate) {
if (dpsChanged) {
this.recalculateDps();
}
if (epsChanged) {
this.recalculateEps();
}
if (hpsChanged) {
this.recalculateHps();
}
if (powerGeneratedChange) {
this.updatePowerGenerated();
}
@@ -815,10 +847,13 @@ export default class Ship {
this.updatePowerUsed();
}
if (armourChange) {
this.updateArmour();
this.recalculateArmour();
}
if (shieldChange) {
this.updateShield();
this.recalculateShield();
}
if (shieldCellsChange) {
this.recalculateShieldCells();
}
this.updateTopSpeed();
this.updateJumpStats();
@@ -826,20 +861,108 @@ export default class Ship {
return this;
}
/**
* Calculate diminishing returns value, where values below a given limit are returned
* as-is, and values between the lower and upper limit of the diminishing returns are
* given at half value.
* Commonly used for resistances.
* @param {Number} val The value
* @param {Number} drll The lower limit for diminishing returns
* @param {Number} drul The upper limit for diminishing returns
* @return {this} The ship instance (for chaining operations)
*/
diminishingReturns(val, drll, drul) {
if (val > drll) {
val = drll + (val - drll) / 2;
}
if (val > drul) {
val = drul;
}
return val;
}
/**
* Calculate damage per second for weapons
* @return {this} The ship instance (for chaining operations)
*/
recalculateDps() {
let totalDpe = 0;
let totalExplDpe = 0;
let totalKinDpe = 0;
let totalThermDpe = 0;
let totalDps = 0;
let totalExplDps = 0;
let totalKinDps = 0;
let totalThermDps = 0;
let totalSDps = 0;
let totalExplSDps = 0;
let totalKinSDps = 0;
let totalThermSDps = 0;
for (let slotNum in this.hardpoints) {
const slot = this.hardpoints[slotNum];
if (slot.m && slot.enabled && slot.m.getDps()) {
totalDps += slot.m.getDps();
const dpe = slot.m.getDps() / slot.m.getEps();
const dps = slot.m.getDps();
const sdps = slot.m.getClip() ? (slot.m.getClip() * slot.m.getDps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : dps;
totalDpe += dpe;
totalDps += dps;
totalSDps += sdps;
if (slot.m.type === 'E') {
totalExplDpe += dpe;
totalExplDps += dps;
totalExplSDps += sdps;
}
if (slot.m.type === 'K') {
totalKinDpe += dpe;
totalKinDps += dps;
totalKinSDps += sdps;
}
if (slot.m.type === 'T') {
totalThermDpe += dpe;
totalThermDps += dps;
totalThermSDps += sdps;
}
if (slot.m.type === 'EK') {
totalExplDpe += dpe / 2;
totalKinDpe += dpe / 2;
totalExplDps += dps / 2;
totalKinDps += dps / 2;
totalExplSDps += sdps / 2;
totalKinSDps += sdps / 2;
}
if (slot.m.type === 'ET') {
totalExplDpe += dpe / 2;
totalThermDpe += dpe / 2;
totalExplDps += dps / 2;
totalThermDps += dps / 2;
totalExplSDps += sdps / 2;
totalThermSDps += sdps / 2;
}
if (slot.m.type === 'KT') {
totalKinDpe += dpe / 2;
totalThermDpe += dpe / 2;
totalKinDps += dps / 2;
totalThermDps += dps / 2;
totalKinSDps += sdps / 2;
totalThermSDps += sdps / 2;
}
}
}
this.totalDpe = totalDpe;
this.totalExplDpe = totalExplDpe;
this.totalKinDpe = totalKinDpe;
this.totalThermDpe = totalThermDpe;
this.totalDps = totalDps;
this.totalExplDps = totalExplDps;
this.totalKinDps = totalKinDps;
this.totalThermDps = totalThermDps;
this.totalSDps = totalSDps;
this.totalExplSDps = totalExplSDps;
this.totalKinSDps = totalKinSDps;
this.totalThermSDps = totalThermSDps;
return this;
}
@@ -958,44 +1081,88 @@ export default class Ship {
* Update shield
* @return {this} The ship instance (for chaining operations)
*/
updateShield() {
// Base shield from generator
let baseShield = 0;
let sgSlot = this.findInternalByGroup('sg');
recalculateShield() {
let shield = 0;
let shieldExplRes = null;
let shieldKinRes = null;
let shieldThermRes = null;
const sgSlot = this.findInternalByGroup('sg');
if (sgSlot && sgSlot.enabled) {
baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
}
// Shield from generator
const baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1);
shield = baseShield;
shieldExplRes = 1 - sgSlot.m.getExplosiveResistance();
shieldKinRes = 1 - sgSlot.m.getKineticResistance();
shieldThermRes = 1 - sgSlot.m.getThermalResistance();
let shield = baseShield;
// Shield from boosters
for (let slot of this.hardpoints) {
if (slot.m && slot.m.grp == 'sb') {
shield += baseShield * slot.m.getShieldBoost();
// Shield from boosters
for (let slot of this.hardpoints) {
if (slot.m && slot.m.grp == 'sb') {
shield += baseShield * slot.m.getShieldBoost();
shieldExplRes *= (1 - slot.m.getExplosiveResistance());
shieldKinRes *= (1 - slot.m.getKineticResistance());
shieldThermRes *= (1 - slot.m.getThermalResistance());
}
}
}
this.shield = shield;
this.shieldExplRes = shieldExplRes ? 1 - this.diminishingReturns(1 - shieldExplRes, 0.5, 0.75) : null;
this.shieldKinRes = shieldKinRes ? 1 - this.diminishingReturns(1 - shieldKinRes, 0.5, 0.75) : null;
this.shieldThermRes = shieldThermRes ? 1 - this.diminishingReturns(1 - shieldThermRes, 0.5, 0.75) : null;
return this;
}
/**
* Update armour
* Update shield cells
* @return {this} The ship instance (for chaining operations)
*/
updateArmour() {
recalculateShieldCells() {
let shieldCells = 0;
for (let slot of this.internal) {
if (slot.m && slot.m.grp == 'scb') {
shieldCells += slot.m.getShieldReinforcement() * slot.m.getCells();
}
}
this.shieldCells = shieldCells;
return this;
}
/**
* Update armour and hull resistances
* @return {this} The ship instance (for chaining operations)
*/
recalculateArmour() {
// Armour from bulkheads
let bulkhead = this.bulkheads.m;
let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost());
let hullExplRes = 1 - bulkhead.getExplosiveResistance();
let hullKinRes = 1 - bulkhead.getKineticResistance();
let hullThermRes = 1 - bulkhead.getThermalResistance();
// Armour from HRPs
for (let slot of this.internal) {
if (slot.m && slot.m.grp == 'hr') {
armour += slot.m.getHullReinforcement();
// Hull boost for HRPs is applied against the ship's base armour
armour += this.baseArmour * slot.m.getModValue('hullboost');
armour += this.baseArmour * slot.m.getModValue('hullboost') / 10000;
hullExplRes *= (1 - slot.m.getExplosiveResistance());
hullKinRes *= (1 - slot.m.getKineticResistance());
hullThermRes *= (1 - slot.m.getThermalResistance());
}
}
this.armour = armour;
this.hullExplRes = 1 - this.diminishingReturns(1 - hullExplRes, 0.5, 0.75);
this.hullKinRes = 1 - this.diminishingReturns(1 - hullKinRes, 0.5, 0.75);
this.hullThermRes = 1 - this.diminishingReturns(1 - hullThermRes, 0.5, 0.75);
return this;
}
@@ -1032,7 +1199,7 @@ export default class Ship {
priorities.push(slot.priority);
}
this.serialized.priorities = LZString.compressToBase64(priorities.join('')).replace(/\//g, '-');
this.serialized.priorities = LZString.compressToBase64(priorities.join(''));
return this;
}
@@ -1053,7 +1220,7 @@ export default class Ship {
enabled.push(slot.enabled ? 1 : 0);
}
this.serialized.enabled = LZString.compressToBase64(enabled.join('')).replace(/\//g, '-');
this.serialized.enabled = LZString.compressToBase64(enabled.join(''));
return this;
}
@@ -1067,7 +1234,7 @@ export default class Ship {
let bulkheadMods = new Array();
if (this.bulkheads.m && this.bulkheads.m.mods) {
for (let modKey in this.bulkheads.m.mods) {
bulkheadMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(this.bulkheads.m.getModValue(modKey) * 10000));
bulkheadMods.push(Modifications.modifications.indexOf(modKey) + ':' + this.bulkheads.m.getModValue(modKey));
}
}
allMods.push(bulkheadMods.join(';'));
@@ -1076,7 +1243,7 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
@@ -1085,7 +1252,7 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
@@ -1094,12 +1261,12 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + Math.round(slot.m.getModValue(modKey) * 10000));
slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey));
}
}
allMods.push(slotMods.join(';'));
}
this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, '')).replace(/\//g, '-');
this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, ''));
return this;
}
@@ -1117,7 +1284,7 @@ export default class Ship {
for (let j = 0; j < mods.length; j++) {
let modElements = mods[j].split(':');
if (modElements[0].match('[0-9]+')) {
arr[i][Modifications.modifiers[modElements[0]]] = Number(modElements[1]);
arr[i][Modifications.modifications[modElements[0]]] = Number(modElements[1]);
} else {
arr[i][modElements[0]] = Number(modElements[1]);
}
@@ -1131,7 +1298,7 @@ export default class Ship {
* This is a binary structure. It starts with a byte that identifies a slot, with bulkheads being ID 0 and moving through
* standard modules, hardpoints, and finally internal modules. It then contains one or more modifications, with each
* modification being a one-byte modification ID and at two-byte modification value. Modification IDs are based on the array
* in Modifications.modifiers. The list of modifications is terminated by a modification ID of -1. The structure then repeats
* in Modifications.modifications. The list of modifications is terminated by a modification ID of -1. The structure then repeats
* for the next module, and the next, and is terminated by a slot ID of -1.
* @return {this} The ship instance (for chaining operations)
*/
@@ -1142,7 +1309,10 @@ export default class Ship {
let bulkheadMods = new Array();
if (this.bulkheads.m && this.bulkheads.m.mods) {
for (let modKey in this.bulkheads.m.mods) {
bulkheadMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(this.bulkheads.m.getModValue(modKey) * 10000) });
// Filter out invalid modifications
if (Modifications.validity['bh'] && Modifications.validity['bh'].indexOf(modKey) != -1) {
bulkheadMods.push({ id: Modifications.modifications.indexOf(modKey), value: this.bulkheads.m.getModValue(modKey) });
}
}
}
slots.push(bulkheadMods);
@@ -1151,7 +1321,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
// Filter out invalid modifications
if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
}
}
}
slots.push(slotMods);
@@ -1161,7 +1334,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
// Filter out invalid modifications
if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
}
}
}
slots.push(slotMods);
@@ -1171,7 +1347,10 @@ export default class Ship {
let slotMods = new Array();
if (slot.m && slot.m.mods) {
for (let modKey in slot.m.mods) {
slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) });
// Filter out invalid modifications
if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) {
slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) });
}
}
}
slots.push(slotMods);
@@ -1197,6 +1376,7 @@ export default class Ship {
for (let slotMod of slot) {
buffer.writeInt8(slotMod.id, curpos++);
buffer.writeInt32LE(slotMod.value, curpos);
// console.log('ENCODE Slot ' + i + ': ' + Modifications.modifications[slotMod.id] + ' = ' + slotMod.value);
curpos += 4;
}
buffer.writeInt8(-1, curpos++);
@@ -1207,7 +1387,7 @@ export default class Ship {
buffer.writeInt8(-1, curpos++);
}
this.serialized.modifications = zlib.gzipSync(buffer).toString('base64').replace(/\//g, '-');
this.serialized.modifications = zlib.gzipSync(buffer).toString('base64');
} else {
this.serialized.modifications = null;
}
@@ -1229,7 +1409,8 @@ export default class Ship {
while (modificationId != -1) {
let modificationValue = buffer.readInt32LE(curpos);
curpos += 4;
modifications[Modifications.modifiers[modificationId]] = modificationValue;
// console.log('DECODE Slot ' + slot + ': ' + Modifications.modifications[modificationId] + ' = ' + modificationValue);
modifications[Modifications.modifications[modificationId]] = modificationValue;
modificationId = buffer.readInt8(curpos++);
}
arr[slot] = modifications;

View File

@@ -11,6 +11,7 @@ const LS_KEY_MOD_DISCOUNT = 'moduleDiscount';
const LS_KEY_STATE = 'state';
const LS_KEY_SIZE_RATIO = 'sizeRatio';
const LS_KEY_TOOLTIPS = 'tooltips';
const LS_KEY_MODULE_RESISTANCES = 'moduleResistances';
let LS;
@@ -81,6 +82,7 @@ export class Persist extends EventEmitter {
LS = null;
}
let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES);
let tips = _get(LS_KEY_TOOLTIPS);
let insurance = _getString(LS_KEY_INSURANCE);
let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT);
@@ -99,6 +101,7 @@ export class Persist extends EventEmitter {
this.state = _get(LS_KEY_STATE);
this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1;
this.tooltipsEnabled = tips === null ? true : tips;
this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances;
if (LS) {
window.addEventListener('storage', this.onStorageChange);
@@ -143,6 +146,10 @@ export class Persist extends EventEmitter {
this.tooltipsEnabled = !!newValue && newValue.toLowerCase() == 'true';
this.emit('tooltips', this.tooltipsEnabled);
break;
case LS_KEY_MODULE_RESISTANCES:
this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true';
this.emit('moduleresistances', this.moduleResistancesEnabled);
break;
}
} catch (e) {
// On JSON.Parse Error - don't sync or do anything
@@ -183,6 +190,21 @@ export class Persist extends EventEmitter {
return this.tooltipsEnabled;
}
/**
* Show module resistances setting
* @param {boolean} show Optional - update setting
* @return {boolean} True if module resistances should be shown
*/
showModuleResistances(show) {
if (show !== undefined) {
this.moduleResistancesEnabled = !!show;
_put(LS_KEY_MODULE_RESISTANCES, this.moduleResistancesEnabled);
this.emit('moduleresistances', this.moduleResistancesEnabled);
}
return this.moduleResistancesEnabled;
}
/**
* Persist a ship build in local storage.
*

View File

@@ -0,0 +1,367 @@
import React from 'react';
import { Modifications, Modules, Ships } from 'coriolis-data/dist';
import Module from '../shipyard/Module';
import Ship from '../shipyard/Ship';
// mapping from fd's ship model names to coriolis'
const SHIP_FD_NAME_TO_CORIOLIS_NAME = {
'Adder': 'adder',
'Anaconda': 'anaconda',
'Asp': 'asp',
'Asp_Scout': 'asp_scout',
'BelugaLiner': 'beluga',
'CobraMkIII': 'cobra_mk_iii',
'CobraMkIV': 'cobra_mk_iv',
'Cutter': 'imperial_cutter',
'DiamondBack': 'diamondback_explorer',
'DiamondBackXL': 'diamondback',
'Eagle': 'eagle',
'Empire_Courier': 'imperial_courier',
'Empire_Eagle': 'imperial_eagle',
'Empire_Trader': 'imperial_clipper',
'Federation_Corvette': 'federal_corvette',
'Federation_Dropship': 'federal_dropship',
'Federation_Dropship_MkII': 'federal_assault_ship',
'Federation_Gunship': 'federal_gunship',
'FerDeLance': 'fer_de_lance',
'Hauler': 'hauler',
'Independant_Trader': 'keelback',
'Orca': 'orca',
'Python': 'python',
'SideWinder': 'sidewinder',
'Type6': 'type_6_transporter',
'Type7': 'type_7_transport',
'Type9': 'type_9_heavy',
'Viper': 'viper',
'Viper_MKIV': 'viper_mk_iv',
'Vulture': 'vulture'
};
// Mapping from hardpoint class to name in companion API
const HARDPOINT_NUM_TO_CLASS = {
0: 'Tiny',
1: 'Small',
2: 'Medium',
3: 'Large',
4: 'Huge'
};
/**
* Obtain a module given its ED ID
* @param {Integer} edId the Elite ID of the module
* @return {Module} the module
*/
function _moduleFromEdId(edId) {
if (!edId) return null;
// Check standard modules
for (const grp in Modules.standard) {
if (Modules.standard.hasOwnProperty(grp)) {
for (const i in Modules.standard[grp]) {
if (Modules.standard[grp][i].edID === edId) {
// Found it
return new Module({ template: Modules.standard[grp][i] });
}
}
}
}
// Check hardpoint modules
for (const grp in Modules.hardpoints) {
if (Modules.hardpoints.hasOwnProperty(grp)) {
for (const i in Modules.hardpoints[grp]) {
if (Modules.hardpoints[grp][i].edID === edId) {
// Found it
return new Module({ template: Modules.hardpoints[grp][i] });
}
}
}
}
// Check internal modules
for (const grp in Modules.internal) {
if (Modules.internal.hasOwnProperty(grp)) {
for (const i in Modules.internal[grp]) {
if (Modules.internal[grp][i].edID === edId) {
// Found it
return new Module({ template: Modules.internal[grp][i] });
}
}
}
}
// Not found
return null;
}
/**
* Obtain the model of a ship given its ED name
* @param {string} edName the Elite name of the ship
* @return {string} the Coriolis model of the ship
*/
function _shipModelFromEDName(edName) {
return SHIP_FD_NAME_TO_CORIOLIS_NAME[edName];
}
/**
* Obtain a ship's model from the companion API JSON
* @param {object} json the companion API JSON
* @return {string} the Coriolis model of the ship
*/
export function shipModelFromJson(json) {
return _shipModelFromEDName(json.name);
}
/**
* Build a ship from the companion API JSON
* @param {object} json the companion API JSON
* @return {Ship} the built ship
*/
export function shipFromJson(json) {
// Start off building a basic ship
const shipModel = shipModelFromJson(json);
if (!shipModel) {
throw 'No such ship found: "' + json.name + '"';
}
const shipTemplate = Ships[shipModel];
let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots);
ship.buildWith(null);
// Set the cargo hatch. We don't have any information on it so guess it's priority 5 and disabled
ship.cargoHatch.enabled = false;
ship.cargoHatch.priority = 4;
// Add the bulkheads
const armourJson = json.modules.Armour.module;
if (armourJson.name.endsWith('_Armour_Grade1')) {
ship.useBulkhead(0, true);
} else if (armourJson.name.endsWith('_Armour_Grade2')) {
ship.useBulkhead(1, true);
} else if (armourJson.name.endsWith('_Armour_Grade3')) {
ship.useBulkhead(2, true);
} else if (armourJson.name.endsWith('_Armour_Mirrored')) {
ship.useBulkhead(3, true);
} else if (armourJson.name.endsWith('_Armour_Reactive')) {
ship.useBulkhead(4, true);
} else {
throw 'Unknown bulkheads "' + armourJson.name + '"';
}
ship.bulkheads.enabled = true;
if (armourJson.modifiers) _addModifications(ship.bulkheads.m, armourJson.modifiers);
// Add the standard modules
// Power plant
const powerplantJson = json.modules.PowerPlant.module;
const powerplant = _moduleFromEdId(powerplantJson.id);
if (powerplantJson.modifiers) _addModifications(powerplant, powerplantJson.modifiers);
ship.use(ship.standard[0], powerplant, true);
ship.standard[0].enabled = powerplantJson.on === true;
ship.standard[0].priority = powerplantJson.priority;
// Thrusters
const thrustersJson = json.modules.MainEngines.module;
const thrusters = _moduleFromEdId(thrustersJson.id);
if (thrustersJson.modifiers) _addModifications(thrusters, thrustersJson.modifiers);
ship.use(ship.standard[1], thrusters, true);
ship.standard[1].enabled = thrustersJson.on === true;
ship.standard[1].priority = thrustersJson.priority;
// FSD
const frameshiftdriveJson = json.modules.FrameShiftDrive.module;
const frameshiftdrive = _moduleFromEdId(frameshiftdriveJson.id);
if (frameshiftdriveJson.modifiers) _addModifications(frameshiftdrive, frameshiftdriveJson.modifiers);
ship.use(ship.standard[2], frameshiftdrive, true);
ship.standard[2].enabled = frameshiftdriveJson.on === true;
ship.standard[2].priority = frameshiftdriveJson.priority;
// Life support
const lifesupportJson = json.modules.LifeSupport.module;
const lifesupport = _moduleFromEdId(lifesupportJson.id);
if (lifesupportJson.modifiers)_addModifications(lifesupport, lifesupportJson.modifiers);
ship.use(ship.standard[3], lifesupport, true);
ship.standard[3].enabled = lifesupportJson.on === true;
ship.standard[3].priority = lifesupportJson.priority;
// Power distributor
const powerdistributorJson = json.modules.PowerDistributor.module;
const powerdistributor = _moduleFromEdId(powerdistributorJson.id);
if (powerdistributorJson.modifiers) _addModifications(powerdistributor, powerdistributorJson.modifiers);
ship.use(ship.standard[4], powerdistributor, true);
ship.standard[4].enabled = powerdistributorJson.on === true;
ship.standard[4].priority = powerdistributorJson.priority;
// Sensors
const sensorsJson = json.modules.Radar.module;
const sensors = _moduleFromEdId(sensorsJson.id);
if (sensorsJson.modifiers) _addModifications(sensors, sensorsJson.modifiers);
ship.use(ship.standard[5], sensors, true);
ship.standard[5].enabled = sensorsJson.on === true;
ship.standard[5].priority = sensorsJson.priority;
// Fuel tank
const fueltankJson = json.modules.FuelTank.module;
const fueltank = _moduleFromEdId(fueltankJson.id);
ship.use(ship.standard[6], fueltank, true);
ship.standard[6].enabled = true;
ship.standard[6].priority = 0;
// Add hardpoints
let hardpointClassNum = -1;
let hardpointSlotNum = -1;
let hardpointArrayNum = 0;
for (let i in shipTemplate.slots.hardpoints) {
if (shipTemplate.slots.hardpoints[i] === hardpointClassNum) {
// Another slot of the same class
hardpointSlotNum++;
} else {
// The first slot of a new class
hardpointClassNum = shipTemplate.slots.hardpoints[i];
hardpointSlotNum = 1;
}
// Now that we know what we're looking for, find it
const hardpointName = HARDPOINT_NUM_TO_CLASS[hardpointClassNum] + 'Hardpoint' + hardpointSlotNum;
const hardpointSlot = json.modules[hardpointName];
if (!hardpointSlot.module) {
// No module
} else {
const hardpointJson = hardpointSlot.module;
const hardpoint = _moduleFromEdId(hardpointJson.id);
if (hardpointJson.modifiers) _addModifications(hardpoint, hardpointJson.modifiers);
ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true);
ship.hardpoints[hardpointArrayNum].enabled = hardpointJson.on === true;
ship.hardpoints[hardpointArrayNum].priority = hardpointJson.priority;
}
hardpointArrayNum++;
}
// Add internal compartments
let internalSlotNum = 1;
for (let i in shipTemplate.slots.internal) {
const internalClassNum = shipTemplate.slots.internal[i];
let internalSlot = null;
while (internalSlot === null && internalSlotNum < 99) {
// Slot numbers are not contiguous so handle skips
const internalName = 'Slot' + (internalSlotNum <= 9 ? '0' : '') + internalSlotNum + '_Size' + internalClassNum;
if (json.modules[internalName]) {
internalSlot = json.modules[internalName];
}
internalSlotNum++;
}
if (!internalSlot.module) {
// No module
} else {
const internalJson = internalSlot.module;
const internal = _moduleFromEdId(internalJson.id);
if (internalJson.modifiers) _addModifications(internal, internalJson.modifiers);
ship.use(ship.internal[i], internal, true);
ship.internal[i].enabled = internalJson.on === true;
ship.internal[i].priority = internalJson.priority;
}
}
// Now update the ship's codes before returning it
return ship.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString();
}
/**
* Add the modifications for a module
* @param {Module} module the module
* @param {Object} modifiers the modifiers
*/
function _addModifications(module, modifiers) {
if (!modifiers || !modifiers.modifiers) return;
for (const i in modifiers.modifiers) {
// Look up the modifiers to find what we need to do
const modifierActions = Modifications.modifierActions[modifiers.modifiers[i].name];
const value = modifiers.modifiers[i].value;
// Carry out the required changes
for (const action in modifierActions) {
const actionValue = modifierActions[action] * value;
let mod = module.getModValue(action) / 10000;
if (!mod) {
mod = 0;
}
module.setModValue(action, ((1 + mod) * (1 + actionValue) - 1) * 10000);
}
}
// Need to fix up a few items
// Shield boosters are treated internally as straight modifiers, so rather than (for example)
// being a 4% boost they are a 104% multiplier. Unfortunately this means that our % modification
// is incorrect so we fix it
if (module.grp === 'sb' && module.getModValue('shieldboost')) {
const alteredBoost = (1 + module.shieldboost) * (module.getModValue('shieldboost') / 10000);
module.setModValue('shieldboost', alteredBoost * 10000 / module.shieldboost);
}
// Shield booster resistance is actually a damage modifier, so needs to be inverted.
if (module.grp === 'sb') {
if (module.getModValue('explres')) {
module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000);
}
if (module.getModValue('kinres')) {
module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
}
if (module.getModValue('thermres')) {
module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
}
}
// Shield generator 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.isShieldGenerator()) {
if (module.getModValue('explres')) {
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
}
if (module.getModValue('kinres')) {
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
}
if (module.getModValue('thermres')) {
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
}
}
// Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted.
if (module.grp === 'hr') {
if (module.getModValue('explres')) {
module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000);
}
if (module.getModValue('kinres')) {
module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000);
}
if (module.getModValue('thermres')) {
module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000);
}
}
// Bulkhead 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 == 'bh') {
if (module.getModValue('explres')) {
module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000);
}
if (module.getModValue('kinres')) {
module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000);
}
if (module.getModValue('thermres')) {
module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000);
}
}
// Jitter is an absolute number, so we need to divide it by 100
if (module.getModValue('jitter')) {
module.setModValue('jitter', module.getModValue('jitter') / 100);
}
// FD uses interval between bursts internally, so we need to translate this to a real rate of fire
if (module.getModValue('rof')) {
module.setModValue('rof', ((1 / (1 + module.getModValue('rof') / 10000)) - 1) * 10000);
}
}

View File

@@ -1,4 +1,3 @@
/**
* Generates a URL for the outiffing page
* @param {String} shipId Ship Id
@@ -7,15 +6,18 @@
* @return {String} URL
*/
export function outfitURL(shipId, code, buildName) {
let parts = ['/outfit/', shipId];
let path = '/outfit/' + shipId;
let sepChar = '?';
if (code) {
parts.push('/', code);
path = path + sepChar + 'code=' + encodeURIComponent(code);
sepChar = '&';
}
if (buildName) {
parts.push('?bn=', encodeURIComponent(buildName));
path = path + sepChar + 'bn=' + encodeURIComponent(buildName);
}
return parts.join('');
}
return path;
}

View File

@@ -58,3 +58,16 @@ export function shallowEqual(objA, objB) {
return true;
}
/**
* Turn a URL-safe base-64 encoded string in to a normal version.
* Coriolis used to use a different encoding system, and some old
* data might be bookmarked or on local storage, so we keep this
* around and use it when decoding data from the old-style URLs to
* be safe.
* @param {string} data the string
* @return {string} the converted string
*/
export function fromUrlSafe(data) {
return data ? data.replace(/-/g, '/').replace(/_/g, '+') : null;
}

View File

@@ -56,3 +56,9 @@
height: 2em;
}
}
.summary {
stroke: @fg;
stroke-width: 10;
fill: @fg;
}

View File

@@ -275,15 +275,15 @@
"minimum": 1
},
"hullExplRes": {
"description": "Resistance of the hull to explosive attacks",
"description": "Multiplier for explosive damage to hull",
"type": "number"
},
"hullKinRes": {
"description": "Resistance of the hull to kinetic attacks",
"description": "Multiplier for kinetic damage to hull",
"type": "number"
},
"hullThermRes": {
"description": "Resistance of the hull to thermal attacks",
"description": "Multiplier for thermal damage to hull",
"type": "number"
},
"fuelCapacity": {
@@ -311,20 +311,20 @@
"minimum": 1
},
"shield": {
"description": "Shield strengh in Mega Joules (Mj)",
"description": "Shield strength in Mega Joules (Mj)",
"type": "number",
"minimum": 0
},
"shieldExplRes": {
"description": "Resistance of the shield to explosive attacks",
"description": "Multiplier for explosive damage to shields",
"type": "number"
},
"shieldKinRes": {
"description": "Resistance of the shield to kinetic attacks",
"description": "Multiplier for kinetic damage to shields",
"type": "number"
},
"shieldThermRes": {
"description": "Resistance of the shield to thermal attacks",
"description": "Multiplier for thermal damage to shields",
"type": "number"
},
"speed": {
@@ -333,8 +333,8 @@
"minimum": 1
},
"totalCost": {
"type": "integer",
"minimum": 1
"description": "Total cost of the loadout, including discounts",
"type": "number"
},
"unladenRange": {
"description": "Single Jump range when unladen, see unladenMass",