diff --git a/package.json b/package.json index 4c12a8b1..674af442 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coriolis_shipyard", - "version": "2.2.0", + "version": "2.2.1", "repository": { "type": "git", "url": "https://github.com/EDCD/coriolis" @@ -32,6 +32,7 @@ ], "automock": true, "unmockedModulePathPatterns": [ + "/node_modules/lodash", "/node_modules/react", "/node_modules/react-dom", "/node_modules/react-addons-test-utils", @@ -86,7 +87,9 @@ "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": "^15.0.1", "react-dom": "^15.0.1", "superagent": "^1.4.0" diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index 534a722a..f72a475d 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -97,7 +97,7 @@ export default class AvailableModulesMenu extends TranslatedComponent { let m = modules[i]; let mount = null; let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; - let active = mountedModule && mountedModule === m; + let active = mountedModule && mountedModule.id === m.id; let classes = cn(m.name ? 'lc' : 'c', { warning: !disabled && warningFunc && warningFunc(m), active, diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index dd55be07..9ecc6dae 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -1,6 +1,9 @@ import React from 'react'; import Slot from './Slot'; -import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications } from './SvgIcons'; +import { Modifications } from 'coriolis-data/dist'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; + /** * Hardpoint / Utility Slot @@ -36,29 +39,31 @@ export default class HardpointSlot extends Slot { if (m) { let classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`; let { drag, drop } = this.props; + let { termtip, tooltip } = this.context; + let validMods = Modifications.validity[m.grp] || []; return
- {m.mount && m.mount == 'F' ? : ''} - {m.mount && m.mount == 'G' ? : ''} - {m.mount && m.mount == 'T' ? : ''} - {m.type && m.type == 'K' ? : ''} - {m.type && m.type == 'T' ? : ''} - {m.type && m.type == 'KT' ? : ''} - {m.type && m.type == 'E' ? : ''} + {m.mount && m.mount == 'F' ? : ''} + {m.mount && m.mount == 'G' ? : ''} + {m.mount && m.mount == 'T' ? : ''} + {m.type && m.type.match('K') ? : ''} + {m.type && m.type.match('T') ? : ''} + {m.type && m.type.match('E') ? : ''} {classRating} {translate(m.name || m.grp)}
-
{m.mass}{u.T}
+
{formats.round(m.getMass())}{u.T}
- { m.dps ?
{translate('DPS')}: {formats.round1(m.dps)} { m.clip ? ({formats.round1((m.clip * m.dps / m.rof) / ((m.clip / m.rof) + m.reload)) }) : null }
: null } - { m.eps ?
{translate('EPS')}: {formats.round1(m.eps)} { m.clip ? ({formats.round1((m.clip * m.eps / m.rof) / ((m.clip / m.rof) + m.reload)) }) : null }
: null } - { m.hps ?
{translate('HPS')}: {formats.round1(m.hps)} { m.clip ? ({formats.round1((m.clip * m.hps / m.rof) / ((m.clip / m.rof) + m.reload)) }) : null }
: null } - { m.dps && m.eps ?
{translate('DPE')}: {formats.round1(m.dps / m.eps)}
: null } - { m.rof ?
{translate('ROF')}: {m.rof}{u.ps}
: null } - { m.range && !m.dps ?
{translate('Range')} : {formats.round(m.range / 1000)}{u.km}
: null } - { m.shieldmul ?
+{formats.rPct(m.shieldmul)}
: null } - { m.ammo >= 0 ?
{translate('ammo')}: {formats.int(m.clip)}/{formats.int(m.ammo)}
: null } + { m.getDps() ?
{translate('DPS')}: {formats.round1(m.getDps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getDps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null } + { m.getEps() ?
{translate('EPS')}: {formats.round1(m.getEps())}{u.MW} { m.getClip() ? ({formats.round1((m.getClip() * m.getEps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }{u.MW}) : null }
: null } + { m.getHps() ?
{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null } + { m.getDps() && m.getEps() ?
{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
: null } + { m.getRoF() ?
{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
: null } + { m.getRange() && !m.getDps() ?
{translate('Range')} : {formats.round(m.getRange() / 1000)}{u.km}
: null } + { m.getShieldBoost() ?
+{formats.pct1(m.getShieldBoost())}
: null } + { m.getAmmo() ?
{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}
: null } + { m && validMods.length > 0 ?
: null }
; } else { diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx index 3c51b3f2..8eb212da 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointsSlotSection.jsx @@ -69,6 +69,7 @@ export default class HardpointsSlotSection extends SlotSection { availableModules={() => availableModules.getHps(h.maxClass)} onOpen={this._openMenu.bind(this, h)} onSelect={this._selectModule.bind(this, h)} + onChange={this.props.onChange} selected={currentMenu == h} drag={this._drag.bind(this, h)} dragOver={this._dragOverSlot.bind(this, h)} @@ -126,6 +127,16 @@ export default class HardpointsSlotSection extends SlotSection {
  • +
    {translate('fc')}
    +
      +
    • +
    • +
    • +
    +
    {translate('nl')}
    +
      +
    • {translate('nl')}
    • +
    ; } diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index dd309546..db2294d5 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -27,7 +27,7 @@ function normalizePercent(val) { if (val === '' || isNaN(val)) { return 0; } - val = Math.round(val * 100) / 100; + val = Math.round(val * 1000) / 1000; return val >= 100 ? 100 : val; } @@ -512,4 +512,4 @@ export default class Header extends TranslatedComponent { ); } -} \ No newline at end of file +} diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index 4c38afd7..318bb85b 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -1,6 +1,8 @@ import React from 'react'; import Slot from './Slot'; -import { Infinite } from './SvgIcons'; +import { ListModifications } from './SvgIcons'; +import { Modifications } from 'coriolis-data/dist'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; /** * Internal Slot @@ -18,30 +20,39 @@ export default class InternalSlot extends Slot { _getSlotDetails(m, translate, formats, u) { if (m) { let classRating = m.class + m.rating; - let { drag, drop } = this.props; + let { drag, drop, ship } = this.props; + let { termtip, tooltip } = this.context; + let validMods = Modifications.validity[m.grp] || []; + let mass = m.getMass() || m.cargo || m.fuel || 0; return
    {classRating} {translate(m.name || m.grp)}
    -
    {m.mass || m.cargo || m.fuel || 0}{u.T}
    +
    {formats.round(mass)}{u.T}
    - { m.optmass ?
    {translate('optimal mass')}: {m.optmass}{u.T}
    : null } - { m.maxmass ?
    {translate('max mass')}: {m.maxmass}{u.T}
    : null } + { m.getOptMass() ?
    {translate('optimal mass')}: {formats.int(m.getOptMass())}{u.T}
    : null } + { m.getMaxMass() ?
    {translate('max mass')}: {formats.int(m.getMaxMass())}{u.T}
    : null } { m.bins ?
    {m.bins} {translate('bins')}
    : null } { m.bays ?
    {translate('bays')}: {m.bays}
    : null } { m.rate ?
    {translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
    : null } - { m.ammo ?
    {translate('ammo')}: {formats.gen(m.ammo)}
    : null } + { m.getAmmo() ?
    {translate('ammunition')}: {formats.gen(m.getAmmo())}
    : null } { m.cells ?
    {translate('cells')}: {m.cells}
    : null } { m.recharge ?
    {translate('recharge')}: {m.recharge} MJ   {translate('total')}: {m.cells * m.recharge}{u.MJ}
    : null } { m.repair ?
    {translate('repair')}: {m.repair}
    : null } - { m.range ?
    {translate('range')} {m.range}{u.km}
    : null } + { m.getFacingLimit() ?
    {translate('facinglimit')} {formats.f1(m.getFacingLimit())}°
    : null } + { m.getRange() ?
    {translate('range')} {formats.f2(m.getRange())}{u.km}
    : null } + { m.getRangeT() ?
    {translate('ranget')} {formats.f1(m.getRangeT())}{u.s}
    : null } + { m.spinup ?
    {translate('spinup')}: {formats.f1(m.spinup)}{u.s}
    : null } { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } { m.maximum ?
    {translate('max')}: {(m.maximum)}
    : null } { m.rangeLS ?
    {translate('range')}: {m.rangeLS}{u.Ls}
    : null } { m.rangeLS === null ?
    ∞{u.Ls}
    : null } { m.rangeRating ?
    {translate('range')}: {m.rangeRating}
    : null } - { m.armouradd ?
    +{m.armouradd} {translate('armour')}
    : null } + { m.getHullReinforcement() ?
    +{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost'))} {translate('armour')}
    : null } + { m.passengers ?
    {translate('passengers')}: {m.passengers}
    : null } + { m && validMods.length > 0 ?
    : null } +
    ; } else { diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index f35d81e4..ae6b8bec 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -16,12 +16,17 @@ export default class InternalSlotSection extends SlotSection { * @param {Object} context React Component context */ constructor(props, context) { - super(props, context, 'internal', 'internal compartments'); + super(props, context, 'internal', 'optional internal'); this._empty = this._empty.bind(this); this._fillWithCargo = this._fillWithCargo.bind(this); this._fillWithCells = this._fillWithCells.bind(this); this._fillWithArmor = this._fillWithArmor.bind(this); + this._fillWithFuelTanks = this._fillWithFuelTanks.bind(this); + this._fillWithLuxuryCabins = this._fillWithLuxuryCabins.bind(this); + this._fillWithFirstClassCabins = this._fillWithFirstClassCabins.bind(this); + this._fillWithBusinessClassCabins = this._fillWithBusinessClassCabins.bind(this); + this._fillWithEconomyClassCabins = this._fillWithEconomyClassCabins.bind(this); } /** @@ -49,6 +54,86 @@ export default class InternalSlotSection extends SlotSection { this._close(); } + /** + * Fill all slots with fuel tanks + * @param {SyntheticEvent} event Event + */ + _fillWithFuelTanks(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C')); + } + }); + this.props.onChange(); + this._close(); + } + + /** + * Fill all slots with luxury passenger cabins + * @param {SyntheticEvent} event Event + */ + _fillWithLuxuryCabins(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6 + } + }); + this.props.onChange(); + this._close(); + } + + /** + * Fill all slots with first class passenger cabins + * @param {SyntheticEvent} event Event + */ + _fillWithFirstClassCabins(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6 + } + }); + this.props.onChange(); + this._close(); + } + + /** + * Fill all slots with business class passenger cabins + * @param {SyntheticEvent} event Event + */ + _fillWithBusinessClassCabins(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6 + } + }); + this.props.onChange(); + this._close(); + } + + /** + * Fill all slots with economy class passenger cabins + * @param {SyntheticEvent} event Event + */ + _fillWithEconomyClassCabins(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6 + } + }); + this.props.onChange(); + this._close(); + } + /** * Fill all slots with Shield Cell Banks * @param {SyntheticEvent} event Event @@ -99,7 +184,7 @@ export default class InternalSlotSection extends SlotSection { let slots = []; let { currentMenu, ship } = this.props; let { originSlot, targetSlot } = this.state; - let { internal, fuelCapacity, ladenMass } = ship; + let { internal, fuelCapacity } = ship; let availableModules = ship.getAvailableModules(); for (let i = 0, l = internal.length; i < l; i++) { @@ -110,9 +195,11 @@ export default class InternalSlotSection extends SlotSection { maxClass={s.maxClass} availableModules={() => availableModules.getInts(ship, s.maxClass, s.eligible)} onOpen={this._openMenu.bind(this,s)} + onChange={this.props.onChange} onSelect={this._selectModule.bind(this, s)} selected={currentMenu == s} enabled={s.enabled} + eligible={s.eligible} m={s.m} drag={this._drag.bind(this, s)} dragOver={this._dragOverSlot.bind(this, s)} @@ -129,15 +216,21 @@ export default class InternalSlotSection extends SlotSection { /** * Generate the section drop-down menu * @param {Function} translate Translate function + * @param {Function} ship The ship * @return {React.Component} Section menu */ - _getSectionMenu(translate) { + _getSectionMenu(translate, ship) { return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}>
    • {translate('empty all')}
    • {translate('cargo')}
    • {translate('scb')}
    • {translate('hr')}
    • +
    • {translate('ft')}
    • +
    • {translate('pce')}
    • +
    • {translate('pci')}
    • +
    • {translate('pcm')}
    • + { ship.luxuryCabins ?
    • {translate('pcq')}
    • : ''}
    • {translate('PHRASE_ALT_ALL')}
    ; diff --git a/src/app/components/Modification.jsx b/src/app/components/Modification.jsx new file mode 100644 index 00000000..c73d0607 --- /dev/null +++ b/src/app/components/Modification.jsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TranslatedComponent from './TranslatedComponent'; +import cn from 'classnames'; +import NumberEditor from 'react-number-editor'; + +/** + * Modification + */ +export default class ModificationsMenu extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + m: React.PropTypes.object.isRequired, + name: React.PropTypes.string.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.state = {}; + this.state.value = this.props.m.getModValue(this.props.name) * 100 || 0; + } + + /** + * Update modification given a value. + * @param {Number} value The value to set + */ + _updateValue(value) { + let scaledValue = Math.floor(Number(value) * 100) / 10000; + // Limit to +1000% / -100% + if (scaledValue > 10) { + scaledValue = 10; + value = 1000; + } + if (scaledValue < -1) { + scaledValue = -1; + value = -100; + } + let m = this.props.m; + let name = this.props.name; + let ship = this.props.ship; + ship.setModification(m, name, scaledValue); + + this.setState({ value }); + this.props.onChange(); + } + + /** + * Render the modification + * @return {React.Component} modification + */ + render() { + let translate = this.context.language.translate; + let name = this.props.name; + + return ( +
    +
    {translate(name)}{name === 'jitter' ? ' (°)' : ' (%)'}
    + +
    + ); + } +} diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx new file mode 100644 index 00000000..bdf633d3 --- /dev/null +++ b/src/app/components/ModificationsMenu.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TranslatedComponent from './TranslatedComponent'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import cn from 'classnames'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import { Modifications } from 'coriolis-data/dist'; +import Modification from './Modification'; + +/** + * Modifications menu + */ +export default class ModificationsMenu extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + m: React.PropTypes.object.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.state = this._initState(props, context); + } + + /** + * Initiate the list of modifications + * @param {Object} props React Component properties + * @param {Object} context React Component context + * @return {Object} list: Array of React Components + */ + _initState(props, context) { + let { m, onChange, ship } = props; + let list = []; + + for (let modName of Modifications.validity[m.grp]) { + list.push(); + } + + return { list }; + } + + /** + * Render the list + * @return {React.Component} List + */ + render() { + return ( +
    e.stopPropagation() } + onContextMenu={stopCtxPropagation} + > + {this.state.list} +
    + ); + } +} diff --git a/src/app/components/PowerManagement.jsx b/src/app/components/PowerManagement.jsx index 0345d895..84b66bee 100644 --- a/src/app/components/PowerManagement.jsx +++ b/src/app/components/PowerManagement.jsx @@ -71,7 +71,7 @@ export default class PowerManagement extends TranslatedComponent { case 'n': comp = comp(null, desc); break; case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break; case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break; - case 'pwr': comp = comp((a, b) => a.m.power - b.m.power, desc); break; + case 'pwr': comp = comp((a, b) => a.m.getPowerUsage() - b.m.getPowerUsage(), desc); break; case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break; case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break; } @@ -113,7 +113,7 @@ export default class PowerManagement extends TranslatedComponent { for (let i = 0, l = ship.powerList.length; i < l; i++) { let slot = ship.powerList[i]; - if (slot.m && slot.m.power) { + if (slot.m && slot.m.getPowerUsage() > 0) { let m = slot.m; let toggleEnabled = this._toggleEnabled.bind(this, slot); let retractedElem = null, deployedElem = null; @@ -134,8 +134,8 @@ export default class PowerManagement extends TranslatedComponent { {' ' + (slot.priority + 1) + ' '} - {pwr(m.power)} - {pct(m.power / ship.powerAvailable)} + {pwr(m.getPowerUsage())} + {pct(m.getPowerUsage() / ship.powerAvailable)} {retractedElem} {deployedElem} ); @@ -214,7 +214,7 @@ export default class PowerManagement extends TranslatedComponent { {translate('pp')} {translate('SYS')} 1 - {pwr(pp.pGen)} + {pwr(pp.getPowerGeneration())} 100% @@ -223,7 +223,7 @@ export default class PowerManagement extends TranslatedComponent { {this._renderPowerRows(ship, translate, pwr, formats.pct1)} - + ); } diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 2e8e57b0..e5069369 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -23,35 +23,40 @@ export default class ShipSummaryTable extends TranslatedComponent { let translate = language.translate; let u = language.units; let formats = language.formats; - let round = formats.round; - let { time, int } = formats; - let armourDetails = null; - let sgClassNames = cn({ warning: ship.sgSlot && !ship.shieldStrength, muted: !ship.sgSlot }); + let { time, int, round, f1, f2, pct } = formats; + let sgClassNames = cn({ warning: ship.findInternalByGroup('sg') && !ship.shield, muted: !ship.findInternalByGroup('sg') }); let sgRecover = '-'; let sgRecharge = '-'; let hide = tooltip.bind(null, null); - if (ship.armourMultiplier > 1 || ship.armourAdded) { - armourDetails = ({ - (ship.armourMultiplier > 1 ? formats.rPct(ship.armourMultiplier) : '') + - (ship.armourAdded ? ' + ' + ship.armourAdded : '') - }); - } - - if (ship.shieldStrength) { + if (ship.shield) { sgRecover = time(ship.calcShieldRecovery()); sgRecharge = time(ship.calcShieldRecharge()); } + // {translate('shield resistance')} + // {translate('hull resistance')} + // {translate('explosive')} + // {translate('kinetic')} + // {translate('thermal')} + // {translate('explosive')} + // {translate('kinetic')} + // {translate('thermal')} + // {pct(ship.shieldExplRes)} + // {pct(ship.shieldKinRes)} + // {pct(ship.shieldThermRes)} + // {pct(ship.hullExplRes)} + // {pct(ship.hullKinRes)} + // {pct(ship.hullThermRes)} return
    - - + + @@ -78,26 +83,26 @@ export default class ShipSummaryTable extends TranslatedComponent { - - - - - + + + + + - - + + - - - - - - + + + + + + diff --git a/src/app/components/Slot.jsx b/src/app/components/Slot.jsx index f0d64ccb..b1c66ba1 100644 --- a/src/app/components/Slot.jsx +++ b/src/app/components/Slot.jsx @@ -2,8 +2,12 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import cn from 'classnames'; import AvailableModulesMenu from './AvailableModulesMenu'; +import ModificationsMenu from './ModificationsMenu'; +import { Modifications } from 'coriolis-data/dist'; +import { ListModifications } from './SvgIcons'; import { diffDetails } from '../utils/SlotFunctions'; import { wrapCtxMenu } from '../utils/UtilityFunctions'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; /** * Abstract Slot @@ -18,6 +22,7 @@ export default class Slot extends TranslatedComponent { selected: React.PropTypes.bool, m: React.PropTypes.object, ship: React.PropTypes.object.isRequired, + eligible: React.PropTypes.object, warning: React.PropTypes.func, drag: React.PropTypes.func, drop: React.PropTypes.func, @@ -31,6 +36,8 @@ export default class Slot extends TranslatedComponent { constructor(props) { super(props); + this._modificationsSelected = false; + this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this)); this._getMaxClassLabel = this._getMaxClassLabel.bind(this); } @@ -73,25 +80,39 @@ export default class Slot extends TranslatedComponent { render() { let language = this.context.language; let translate = language.translate; - let { ship, m, dropClass, dragOver, onOpen, selected, onSelect, warning, shipMass, availableModules } = this.props; + let { ship, m, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props; let slotDetails, menu; + if (!selected) { + // If not selected then sure that modifications flag is unset + this._modificationsSelected = false; + } + if (m) { slotDetails = this._getSlotDetails(m, translate, language.formats, language.units); // Must be implemented by sub classes } else { - slotDetails =
    {translate('empty')}
    ; + slotDetails =
    {translate(eligible ? 'emptyrestricted' : 'empty')}
    ; } - if (this.props.selected) { - menu = ; + if (selected) { + if (this._modificationsSelected) { + menu = ; + } else { + menu = ; + } } // TODO: implement touch dragging @@ -100,10 +121,17 @@ export default class Slot extends TranslatedComponent {
    {this._getMaxClassLabel(translate)}
    - {slotDetails} -
    + {slotDetails} +
    {menu} ); } + + /** + * Toggle the modifications flag when selecting the modifications icon + */ + _toggleModifications() { + this._modificationsSelected = !this._modificationsSelected; + } } diff --git a/src/app/components/SlotSection.jsx b/src/app/components/SlotSection.jsx index 29ea806e..4a339a33 100644 --- a/src/app/components/SlotSection.jsx +++ b/src/app/components/SlotSection.jsx @@ -180,7 +180,7 @@ export default class SlotSection extends TranslatedComponent {

    {translate(this.sectionName)}

    - {sectionMenuOpened ? this._getSectionMenu(translate) : null } + {sectionMenuOpened ? this._getSectionMenu(translate, this.props.ship) : null }
    {this._getSlots()}
    diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 00d81ac3..8140f7f0 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -4,6 +4,10 @@ import TranslatedComponent from './TranslatedComponent'; import { jumpRange } from '../shipyard/Calculations'; import { diffDetails } from '../utils/SlotFunctions'; import AvailableModulesMenu from './AvailableModulesMenu'; +import ModificationsMenu from './ModificationsMenu'; +import { ListModifications } from './SvgIcons'; +import { Modifications } from 'coriolis-data/dist'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; /** * Standard Slot @@ -15,53 +19,83 @@ export default class StandardSlot extends TranslatedComponent { modules: React.PropTypes.array.isRequired, onSelect: React.PropTypes.func.isRequired, onOpen: React.PropTypes.func.isRequired, + onChange: React.PropTypes.func.isRequired, ship: React.PropTypes.object.isRequired, selected: React.PropTypes.bool, warning: React.PropTypes.func, }; + /** + * Construct the slot + * @param {object} props Object properties + */ + constructor(props) { + super(props); + this._modificationsSelected = false; + } + /** * Render the slot * @return {React.Component} Slot component */ render() { + let { termtip, tooltip } = this.context; let { translate, formats, units } = this.context.language; - let { modules, slot, warning, onSelect, ladenMass, ship } = this.props; + let { modules, slot, selected, warning, onSelect, onChange, ship } = this.props; let m = slot.m; let classRating = m.class + m.rating; let menu; + let validMods = m == null ? [] : (Modifications.validity[m.grp] || []); + let mass = m.getMass() || m.cargo || m.fuel || 0; - if (this.props.selected) { - menu = ; + if (!selected) { + // If not selected then sure that modifications flag is unset + this._modificationsSelected = false; + } + + if (selected) { + if (this._modificationsSelected) { + menu = ; + } else { + menu = ; + } } return ( -
    +
    {slot.maxClass}
    {classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}
    -
    {m.mass || m.fuel || 0}{units.T}
    +
    {formats.round(mass)}{units.T}
    +
    { m.grp == 'bh' && m.name ?
    {translate(m.name)}
    : null } - { m.optmass ?
    {translate('optimal mass')}: {m.optmass}{units.T}
    : null } - { m.maxmass ?
    {translate('max mass')}: {m.maxmass}{units.T}
    : null } - { m.range ?
    {translate('range')}: {m.range}{units.km}
    : null } + { m.getOptimalMass() ?
    {translate('optimal mass')}: {formats.int(m.getOptimalMass())}{units.T}
    : null } + { m.getMaxMass() ?
    {translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
    : null } + { m.getRange() ?
    {translate('range')}: {formats.f2(m.getRange())}{units.km}
    : null } { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } - { m.eff ?
    {translate('efficiency')}: {m.eff}
    : null } - { m.pGen ?
    {translate('power')}: {m.pGen}{units.MW}
    : null } - { m.maxfuel ?
    {translate('max')} {translate('fuel')}: {m.maxfuel}{units.T}
    : null } - { m.weaponcapacity ?
    {translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}
    : null } - { m.systemcapacity ?
    {translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}
    : null } - { m.enginecapacity ?
    {translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}
    : null } + { m.getThermalEfficiency() ?
    {translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}
    : null } + { m.getPowerGeneration() > 0 ?
    {translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}
    : null } + { m.getMaxFuelPerJump() ?
    {translate('max')} {translate('fuel')}: {formats.f1(m.getMaxFuelPerJump())}{units.T}
    : null } + { m.getWeaponsCapacity() ?
    {translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}
    : null } + { m.getSystemsCapacity() ?
    {translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}
    : null } + { m.getEnginesCapacity() ?
    {translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}
    : null } + + { validMods.length > 0 ?
    : null }
    @@ -69,4 +103,11 @@ export default class StandardSlot extends TranslatedComponent {
    ); } + + /** + * Toggle the modifications flag when selecting the modifications icon + */ + _toggleModifications() { + this._modificationsSelected = !this._modificationsSelected; + } } diff --git a/src/app/components/StandardSlotSection.jsx b/src/app/components/StandardSlotSection.jsx index ef6b0dc3..48e53656 100644 --- a/src/app/components/StandardSlotSection.jsx +++ b/src/app/components/StandardSlotSection.jsx @@ -18,7 +18,7 @@ export default class StandardSlotSection extends SlotSection { * @param {Object} context React Component context */ constructor(props, context) { - super(props, context, 'standard', 'standard'); + super(props, context, 'standard', 'core internal'); this._optimizeStandard = this._optimizeStandard.bind(this); this._selectBulkhead = this._selectBulkhead.bind(this); } @@ -86,12 +86,10 @@ export default class StandardSlotSection extends SlotSection { * @return {Array} Array of Slots */ _getSlots() { - let { translate, units, formats } = this.context.language; let { ship, currentMenu } = this.props; let slots = new Array(8); let open = this._openMenu; let select = this._selectModule; - let selBulkhead = this._selectBulkhead; let st = ship.standard; let avail = ship.getAvailableModules().standard; let bh = ship.bulkheads; @@ -103,6 +101,7 @@ export default class StandardSlotSection extends SlotSection { onOpen={open.bind(this, bh)} onSelect={this._selectBulkhead} selected={currentMenu == bh} + onChange={this.props.onChange} ship={ship} />; @@ -113,8 +112,9 @@ export default class StandardSlotSection extends SlotSection { onOpen={open.bind(this, st[0])} onSelect={select.bind(this, st[0])} selected={currentMenu == st[0]} + onChange={this.props.onChange} ship={ship} - warning={m => m.pGen < ship.powerRetracted} + warning={m => m.pgen < ship.powerRetracted} />; slots[2] = m.maxmass < (ship.ladenMass - st[1].mass + m.mass)} />; @@ -135,6 +136,7 @@ export default class StandardSlotSection extends SlotSection { modules={avail[2]} onOpen={open.bind(this, st[2])} onSelect={select.bind(this, st[2])} + onChange={this.props.onChange} ship={ship} selected={currentMenu == st[2]} />; @@ -145,6 +147,7 @@ export default class StandardSlotSection extends SlotSection { modules={avail[3]} onOpen={open.bind(this, st[3])} onSelect={select.bind(this, st[3])} + onChange={this.props.onChange} ship={ship} selected={currentMenu == st[3]} />; @@ -156,8 +159,9 @@ export default class StandardSlotSection extends SlotSection { onOpen={open.bind(this, st[4])} onSelect={select.bind(this, st[4])} selected={currentMenu == st[4]} + onChange={this.props.onChange} ship={ship} - warning= {m => m.enginecapacity < ship.boostEnergy} + warning= {m => m.engcap < ship.boostEnergy} />; slots[6] = m.enginecapacity < ship.boostEnergy} />; slots[7] = m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel />; diff --git a/src/app/components/SvgIcons.jsx b/src/app/components/SvgIcons.jsx index 6e228026..50c13e16 100644 --- a/src/app/components/SvgIcons.jsx +++ b/src/app/components/SvgIcons.jsx @@ -488,6 +488,25 @@ export class Rocket extends SvgIcon { } } +/** + * ListModifications (engineers) + */ +export class ListModifications 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 ; + } +} + /** * Hammer */ diff --git a/src/app/components/UtilitySlotSection.jsx b/src/app/components/UtilitySlotSection.jsx index 0e922f83..0351b4ab 100644 --- a/src/app/components/UtilitySlotSection.jsx +++ b/src/app/components/UtilitySlotSection.jsx @@ -68,6 +68,7 @@ export default class UtilitySlotSection extends SlotSection { availableModules={() => availableModules.getHps(h.maxClass)} onOpen={this._openMenu.bind(this,h)} onSelect={this._selectModule.bind(this, h)} + onChange={this.props.onChange} selected={currentMenu == h} drag={this._drag.bind(this, h)} dragOver={this._dragOverSlot.bind(this, h)} @@ -104,9 +105,17 @@ export default class UtilitySlotSection extends SlotSection {
  • B
  • A
  • -
    {translate('cm')}
    +
    {translate('hs')}
      -
    • {translate('Heat Sink Launcher')}
    • +
    • {translate('Heat Sink Launcher')}
    • +
    +
    {translate('ch')}
    +
      +
    • {translate('Chaff Launcher')}
    • +
    +
    {translate('po')}
    +
      +
    • {translate('Point Defence')}
    ; } diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index 6d4d21a8..da82304f 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -46,6 +46,7 @@ export function getLanguage(langCode) { s2: d3Locale.numberFormat('.2s'), // SI Format to 2 decimal places (.e.g 1.1k) pct: d3Locale.numberFormat('.2%'), // % to 2 decimal places (.e.g 5.40%) pct1: d3Locale.numberFormat('.1%'), // % to 1 decimal places (.e.g 5.4%) + r1: d3Locale.numberFormat('.1r'), // Rounded to 1 significant number (.e.g 512 => 500, 4.122 => 4) r2: d3Locale.numberFormat('.2r'), // Rounded to 2 significant numbers (.e.g 512 => 510, 4.122 => 4.1) rPct: d3.format('%'), // % to 0 decimal places (.e.g 5%) round1: (d) => gen(d3.round(d, 1)), // Rounded to 0-1 decimal places (.e.g 5.1, 4) @@ -65,6 +66,7 @@ export function getLanguage(langCode) { MW: {translate('MW')}, // Mega Watts (same as Mega Joules per second) ps: {translate('/s')}, // per second pm: {translate('/min')}, // per minute + s: {translate('secs')}, // Seconds T: {translate('T')}, // Metric Tons } }; diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index c29d0436..6276f0d1 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -37,10 +37,11 @@ export const terms = { bsg: 'Bi-Weave Shield Generator', c: 'Cannon', cc: 'Collector Limpet Controller', - cm: 'Countermeasure', + ch: 'Chaff Launcher', cr: 'Cargo Rack', - cs: 'Cargo Scanner', + cs: 'Manifest Scanner', dc: 'Docking Computer', + ec: 'Electronic Countermeasure', fc: 'Fragment Cannon', fh: 'Fighter Hangar', fi: 'FSD Interdictor', @@ -50,6 +51,7 @@ export const terms = { fx: 'Fuel Transfer Limpet Controller', hb: 'Hatch Breaker Limpet Controller', hr: 'Hull Reinforcement Package', + hs: 'Heat Sink Launcher', kw: 'Kill Warrant Scanner', ls: 'Life Support', mc: 'Multi-cannon', @@ -65,6 +67,7 @@ export const terms = { pcq: 'Luxury Passenger Cabin', pd: 'power distributor', pl: 'Pulse Laser', + po: 'Point Defence', pp: 'Power Plant', psg: 'Prismatic Shield Generator', pv: 'Planetary Vehicle Hangar', @@ -78,5 +81,64 @@ export const terms = { t: 'thrusters', tp: 'Torpedo Pylon', ul: 'Burst Laser', - ws: 'Frame Shift Wake Scanner' + ws: 'Frame Shift Wake Scanner', + + // Items on the outfitting page + // Notification of restricted slot for Orca/Beluga + emptyrestricted: 'empty (restricted)', + // 'ammo' was overloaded for outfitting page and modul info, so changed to ammunition for outfitting page + ammunition: 'Ammo', + + // Unit for seconds + secs: 's', + + // Hardpoint abbreviations + dpe: 'Damage per MJ of energy', + dps: '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)', + + // Modifications + ammo: 'Ammunition maximum', + boot: 'Boot time', + brokenregen: 'Broken regeneration rate', + burst: 'Burst', + clip: 'Ammunition clip', + damage: 'Damage', + distdraw: 'Distributor draw', + duration: 'Duration', + eff: 'Efficiency', + engcap: 'Engines capacity', + engrate: 'Engines recharge rate', + explres: 'Explosive resistance', + facinglimit: 'Facing limit', + hullboost: 'Hull boost', + hullreinforcement: 'Hull reinforcement', + integrity: 'Integrity', + jitter: 'Jitter', + kinres: 'Kinetic resistance', + maxfuel: 'Maximum fuel per jump', + mass: 'Mass', + optmass: 'Optimal mass', + optmul: 'Optimal multiplier', + pgen: 'Power generation', + piercing: 'Piercing', + power: 'Power draw', + range: 'Range', + ranget: 'Range', // Range in time (for FSD interdictor) + regen: 'Regeneration rate', + reload: 'Reload time', + rof: 'Rate of fire', + shield: 'Shield', + shieldboost: 'Shield boost', + spinup: 'Spin up time', + syscap: 'Systems capacity', + sysrate: 'Systems recharge rate', + thermload: 'Thermal load', + thermres: 'Thermal resistance', + wepcap: 'Weapons capacity', + weprate: 'Weapons recharge rate', }; diff --git a/src/app/pages/ComparisonPage.jsx b/src/app/pages/ComparisonPage.jsx index 1ab50fd6..5531525f 100644 --- a/src/app/pages/ComparisonPage.jsx +++ b/src/app/pages/ComparisonPage.jsx @@ -63,7 +63,7 @@ export default class ComparisonPage extends Page { * @return {Object} New state object */ _initState(context) { - let defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost + let defaultFacets = [13, 12, 11, 9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost, DPS, EPS, HPS let params = context.route.params; let code = params.code; let name = params.name ? decodeURIComponent(params.name) : null; diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index e4ee5633..839c6b00 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -284,9 +284,9 @@ export default class OutfittingPage extends Page { canSave = (newBuildName || buildName) && code !== savedCode, canRename = buildName && newBuildName && buildName != newBuildName, canReload = savedCode && canSave, - hStr = ship.getHardpointsString(), - sStr = ship.getStandardString(), - iStr = ship.getInternalString(); + hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(), + sStr = ship.getStandardString() + '.' + ship.getModificationsString(), + iStr = ship.getInternalString() + '.' + ship.getModificationsString(); return (
    diff --git a/src/app/shipyard/Calculations.js b/src/app/shipyard/Calculations.js index 3dd24b3d..96c7b0fe 100644 --- a/src/app/shipyard/Calculations.js +++ b/src/app/shipyard/Calculations.js @@ -1,3 +1,4 @@ +import Module from './Module'; /** * Calculate the maximum single jump range based on mass and a specific FSD @@ -8,7 +9,9 @@ * @return {number} Distance in Light Years */ export function jumpRange(mass, fsd, fuel) { - return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass; + let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + let fsdOptimalMass = fsd instanceof Module ? fsd.getOptimalMass() : fsd.optmass; + return Math.pow(Math.min(fuel === undefined ? fsdMaxFuelPerJump : fuel, fsdMaxFuelPerJump) / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; } /** @@ -20,15 +23,17 @@ export function jumpRange(mass, fsd, fuel) { * @return {number} Distance in Light Years */ export function fastestRange(mass, fsd, fuel) { - let fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps - let jumps = Math.floor(fuel / fsd.maxfuel); + let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + let fsdOptimalMass = fsd instanceof Module ? fsd.getOptimalMass() : fsd.optmass; + let fuelRemaining = fuel % fsdMaxFuelPerJump; // Fuel left after making N max jumps + let jumps = Math.floor(fuel / fsdMaxFuelPerJump); mass += fuelRemaining; // Going backwards, start with the last jump using the remaining fuel - let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass : 0; + let fastestRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass : 0; // For each max fuel jump, calculate the max jump range based on fuel mass left in the tank for (let j = 0; j < jumps; j++) { mass += fsd.maxfuel; - fastestRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower) * fsd.optmass / mass; + fastestRange += Math.pow(fsdMaxFuelPerJump / fsd.fuelmul, 1 / fsd.fuelpower) * fsdOptimalMass / mass; } return fastestRange; }; @@ -36,29 +41,27 @@ export function fastestRange(mass, fsd, fuel) { /** * Calculate the a ships shield strength based on mass, shield generator and shield boosters used. * - * @param {number} mass Current mass of the ship - * @param {number} shields Base Shield strength MJ for ship - * @param {object} sg The shield generator used - * @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any) - * @return {number} Approximate shield strengh in MJ + * @param {number} mass Current mass of the ship + * @param {number} baseShield Base Shield strength MJ for ship + * @param {object} sg The shield generator used + * @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any) + * @return {number} Approximate shield strengh in MJ */ -export function shieldStrength(mass, shields, sg, multiplier) { - let opt; - if (mass < sg.minmass) { - return shields * multiplier * sg.minmul; - } - if (mass > sg.maxmass) { - return shields * multiplier * sg.maxmul; - } - if (mass < sg.optmass) { - opt = (sg.optmass - mass) / (sg.optmass - sg.minmass); - opt = 1 - Math.pow(1 - opt, 0.87); - return shields * multiplier * ((opt * sg.minmul) + ((1 - opt) * sg.optmul)); - } else { - opt = (sg.optmass - mass) / (sg.maxmass - sg.optmass); - opt = -1 + Math.pow(1 + opt, 2.425); - return shields * multiplier * ((-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul)); - } +export function shieldStrength(mass, baseShield, sg, multiplier) { + // sg might be a module or a template; handle either here + let minMass = sg instanceof Module ? sg.getMinMass() : sg.minmass; + let optMass = sg instanceof Module ? sg.getOptMass() : sg.optmass; + let maxMass = sg instanceof Module ? sg.getMaxMass() : sg.maxmass; + let minMul = sg instanceof Module ? sg.getMinMul() : sg.minmul; + let optMul = sg instanceof Module ? sg.getOptMul() : sg.optmul; + let maxMul = sg instanceof Module ? sg.getMaxMul() : sg.maxmul; + + let xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + let exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + let ynorm = Math.pow(xnorm, exponent); + let mul = minMul + ynorm * (maxMul - minMul); + + return baseShield * mul * multiplier; } /** @@ -72,13 +75,24 @@ export function shieldStrength(mass, shields, sg, multiplier) { * @return {object} Approximate speed by pips */ export function speed(mass, baseSpeed, baseBoost, thrusters, pipSpeed) { - let multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P))); - let speed = baseSpeed * multiplier; + // 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() : thrusters.minmul; + let optMul = thrusters instanceof Module ? thrusters.getOptMul() : thrusters.optmul; + let maxMul = thrusters instanceof Module ? thrusters.getMaxMul() : thrusters.maxmul; + + let xnorm = Math.min(1, (maxMass - mass) / (maxMass - minMass)); + let exponent = Math.log((optMul - minMul) / (maxMul - minMul)) / Math.log(Math.min(1, (maxMass - optMass) / (maxMass - minMass))); + let ynorm = Math.pow(xnorm, exponent); + let mul = minMul + ynorm * (maxMul - minMul); + let speed = baseSpeed * mul; return { '0 Pips': speed * (1 - (pipSpeed * 4)), '2 Pips': speed * (1 - (pipSpeed * 2)), '4 Pips': speed, - 'boost': baseBoost * multiplier + 'boost': baseBoost * mul }; } diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index 70688066..566bcf06 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -1,12 +1,4 @@ -export const ArmourMultiplier = [ - 1, // Lightweight - 1.4, // Reinforced - 1.945, // Military - 1.945, // Mirrored - 1.945 // Reactive -]; - export const SizeMap = ['', 'small', 'medium', 'large', 'capital']; export const StandardArray = [ @@ -193,6 +185,20 @@ export const ShipFacets = [ lbls: ['DPS'], fmt: 'round', i: 11 + }, + { // 12 + title: 'EPS', + props: ['totalEps'], + lbls: ['EPS'], + fmt: 'round', + i: 12 + }, + { // 13 + title: 'HPS', + props: ['totalHps'], + lbls: ['HPS'], + fmt: 'round', + i: 13 } ]; diff --git a/src/app/shipyard/Modification.js b/src/app/shipyard/Modification.js new file mode 100755 index 00000000..5c2d4566 --- /dev/null +++ b/src/app/shipyard/Modification.js @@ -0,0 +1,15 @@ +/** + * Modification - a modification and its value + */ +export default class Modification { + + /** + * @param {String} id Unique modification ID + * @param {Number} value Value of the modification + */ + constructor(id, value) { + this.id = id; + this.value = value; + } + +} diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js new file mode 100755 index 00000000..d367cc9a --- /dev/null +++ b/src/app/shipyard/Module.js @@ -0,0 +1,487 @@ +import * as ModuleUtils from './ModuleUtils'; +import * as _ from 'lodash'; + +/** + * Module - active module in a ship's buildout + */ +export default class Module { + + /** + * Construct a new module + * @param {Object} params Module parameters. Either grp/id or template + */ + constructor(params) { + let properties = Object.assign({ grp: null, id: null, template: null }, params); + + let template; + if (properties.template == undefined) { + return ModuleUtils.findModule(properties.grp, properties.id); + } else { + template = properties.template; + if (template) { + // Copy all properties from coriolis-data template + for (let p in template) { this[p] = template[p]; } + } + } + this.mods = {}; + } + + /** + * 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% + */ + getModValue(name) { + return this.mods && this.mods[name] ? this.mods[name] / 10000 : 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% + */ + setModValue(name, value) { + if (value == null || value == 0) { + delete this.mods[name]; + } else { + // Store value with 2dp + this.mods[name] = Math.round(value * 10000); + } + } + + /** + * 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 + */ + _getModifiedValue(name) { + let result = 0; + if (this[name]) { + result = this[name]; + if (result) { + let mult = this.getModValue(name); + if (mult) { result = result * (1 + mult); } + } + } + return result; + } + /** + * Get the power generation of this module, taking in to account modifications + * @return {Number} the power generation of this module + */ + getPowerGeneration() { + return this._getModifiedValue('pgen'); + } + + /** + * Get the power usage of this module, taking in to account modifications + * @return {Number} the power usage of this module + */ + getPowerUsage() { + return this._getModifiedValue('power'); + } + + /** + * Get the integrity of this module, taking in to account modifications + * @return {Number} the integrity of this module + */ + getIntegrity() { + return this._getModifiedValue('integrity'); + } + + /** + * Get the mass of this module, taking in to account modifications + * @return {Number} the mass of this module + */ + getMass() { + return this._getModifiedValue('mass'); + } + + /** + * Get the thermal efficiency of this module, taking in to account modifications + * @return {Number} the thermal efficiency of this module + */ + getThermalEfficiency() { + return this._getModifiedValue('eff'); + } + + /** + * Get the maximum mass of this module, taking in to account modifications + * @return {Number} the maximum mass of this module + */ + getMaxMass() { + return this._getModifiedValue('maxmass'); + } + + /** + * Get the optimal mass of this module, taking in to account modifications + * @return {Number} the optimal mass of this module + */ + getOptimalMass() { + return this._getModifiedValue('optmass'); + } + + /** + * Get the optimal multiplier of this module, taking in to account modifications + * @return {Number} the optimal multiplier of this module + */ + getOptimalMultiplier() { + return this._getModifiedValue('optmult'); + } + + /** + * Get the maximum fuel per jump for this module, taking in to account modifications + * @return {Number} the maximum fuel per jump of this module + */ + getMaxFuelPerJump() { + return this._getModifiedValue('maxfuel'); + } + + /** + * Get the systems capacity for this module, taking in to account modifications + * @return {Number} the systems capacity of this module + */ + getSystemsCapacity() { + return this._getModifiedValue('syscap'); + } + + /** + * Get the engines capacity for this module, taking in to account modifications + * @return {Number} the engines capacity of this module + */ + getEnginesCapacity() { + return this._getModifiedValue('engcap'); + } + + /** + * Get the weapons capacity for this module, taking in to account modifications + * @return {Number} the weapons capacity of this module + */ + getWeaponsCapacity() { + return this._getModifiedValue('wepcap'); + } + + /** + * Get the systems recharge rate for this module, taking in to account modifications + * @return {Number} the systems recharge rate of this module + */ + getSystemsRechargeRate() { + return this._getModifiedValue('sysrate'); + } + + /** + * Get the engines recharge rate for this module, taking in to account modifications + * @return {Number} the engines recharge rate of this module + */ + getEnginesRechargeRate() { + return this._getModifiedValue('engrate'); + } + + /** + * Get the weapons recharge rate for this module, taking in to account modifications + * @return {Number} the weapons recharge rate of this module + */ + getWeaponsRechargeRate() { + return this._getModifiedValue('weprate'); + } + + /** + * Get the kinetic resistance for this module, taking in to account modifications + * @return {Number} the kinetic resistance of this module + */ + getKineticResistance() { + return this._getModifiedValue('kinres'); + } + + /** + * Get the thermal resistance for this module, taking in to account modifications + * @return {Number} the thermal resistance of this module + */ + getThermalResistance() { + return this._getModifiedValue('thermres'); + } + + /** + * Get the explosive resistance for this module, taking in to account modifications + * @return {Number} the explosive resistance of this module + */ + getExplosiveResistance() { + return this._getModifiedValue('explres'); + } + + /** + * Get the regeneration rate for this module, taking in to account modifications + * @return {Number} the regeneration rate of this module + */ + getRegenerationRate() { + return this._getModifiedValue('regen'); + } + + /** + * Get the broken regeneration rate for this module, taking in to account modifications + * @return {Number} the broken regeneration rate of this module + */ + getBrokenRegenerationRate() { + return this._getModifiedValue('brokenregen'); + } + + /** + * Get the range for this module, taking in to account modifications + * @return {Number} the range rate of this module + */ + getRange() { + return this._getModifiedValue('range'); + } + + /** + * Get the range (in terms of seconds, for FSDI) for this module, taking in to account modifications + * @return {Number} the range of this module + */ + getRangeT() { + return this._getModifiedValue('ranget'); + } + + /** + * Get the capture arc for this module, taking in to account modifications + * @return {Number} the capture arc of this module + */ + getCaptureArc() { + return this._getModifiedValue('arc'); + } + + /** + * Get the hull reinforcement for this module, taking in to account modifications + * @return {Number} the hull reinforcement of this module + */ + getHullReinforcement() { + return this._getModifiedValue('hullreinforcement'); + } + + /** + * Get the delay for this module, taking in to account modifications + * @return {Number} the delay of this module + */ + getDelay() { + return this._getModifiedValue('delay'); + } + + /** + * Get the duration for this module, taking in to account modifications + * @return {Number} the duration of this module + */ + getDuration() { + return this._getModifiedValue('duration'); + } + + /** + * Get the shield boost for this module, taking in to account modifications + * @return {Number} the shield boost of this module + */ + getShieldBoost() { + return this._getModifiedValue('shieldboost'); + } + + /** + * Get the minimum mass for this module, taking in to account modifications + * @return {Number} the minimum mass of this module + */ + getMinMass() { + // Modifier is optmass + let result = 0; + if (this['minmass']) { + result = this['minmass']; + if (result) { + let mult = this.getModValue('optmass'); + if (mult) { result = result * (1 + mult); } + } + } + return result; + } + + /** + * Get the optimum mass for this module, taking in to account modifications + * @return {Number} the optimum mass of this module + */ + getOptMass() { + return this._getModifiedValue('optmass'); + } + + /** + * Get the maximum mass for this module, taking in to account modifications + * @return {Number} the maximum mass of this module + */ + getMaxMass() { + // Modifier is optmass + let result = 0; + if (this['maxmass']) { + result = this['maxmass']; + if (result) { + let mult = this.getModValue('optmass'); + if (mult) { result = result * (1 + mult); } + } + } + return result; + } + + /** + * Get the minimum multiplier for this module, taking in to account modifications + * @return {Number} the minimum multiplier of this module + */ + getMinMul() { + // Modifier is optmul + let result = 0; + if (this['minmul']) { + result = this['minmul']; + if (result) { + let mult = this.getModValue('optmul'); + if (mult) { result = result * (1 + mult); } + } + } + return result; + } + + /** + * Get the optimum multiplier for this module, taking in to account modifications + * @return {Number} the optimum multiplier of this module + */ + getOptMul() { + return this._getModifiedValue('optmul'); + } + + /** + * Get the maximum multiplier for this module, taking in to account modifications + * @return {Number} the maximum multiplier of this module + */ + getMaxMul() { + // Modifier is optmul + let result = 0; + if (this['maxmul']) { + result = this['maxmul']; + if (result) { + let mult = this.getModValue('optmul'); + if (mult) { result = result * (1 + mult); } + } + } + return result; + } + + /** + * Get the damage for this module, taking in to account modifications + * @return {Number} the damage of this module + */ + getDamage() { + return this._getModifiedValue('damage'); + } + + /** + * Get the distributor draw for this module, taking in to account modifications + * @return {Number} the distributor draw of this module + */ + getDistDraw() { + return this._getModifiedValue('distdraw'); + } + + /** + * Get the thermal load for this module, taking in to account modifications + * @return {Number} the thermal load of this module + */ + getThermalLoad() { + return this._getModifiedValue('thermload'); + } + + /** + * Get the rounds per shot for this module, taking in to account modifications + * @return {Number} the rounds per shot of this module + */ + getRoundsPerShot() { + return this._getModifiedValue('roundspershot'); + } + + /** + * Get the DPS for this module, taking in to account modifications + * @return {Number} the DPS of this module + */ + getDps() { + // DPS is a synthetic value + let damage = this.getDamage(); + let rpshot = this.getRoundsPerShot() || 1; + let rof = this.getRoF() || 1; + + return damage * rpshot * rof; + } + + /** + * Get the EPS for this module, taking in to account modifications + * @return {Number} the EPS of this module + */ + getEps() { + // EPS is a synthetic value + let distdraw = this.getDistDraw(); + let rpshot = this.getRoundsPerShot() || 1; + let rof = this.getRoF() || 1; + + return distdraw * rpshot * rof; + } + + /** + * Get the HPS for this module, taking in to account modifications + * @return {Number} the HPS of this module + */ + getHps() { + // HPS is a synthetic value + let heat = this.getThermalLoad(); + let rpshot = this.getRoundsPerShot() || 1; + let rof = this.getRoF() || 1; + + return heat * rpshot * rof; + } + + /** + * Get the clip size for this module, taking in to account modifications + * @return {Number} the clip size of this module + */ + getClip() { + return this._getModifiedValue('clip'); + } + + /** + * Get the ammo size for this module, taking in to account modifications + * @return {Number} the ammo size of this module + */ + getAmmo() { + return this._getModifiedValue('ammo'); + } + + /** + * Get the reload time for this module, taking in to account modifications + * @return {Number} the reload time of this module + */ + getReload() { + return this._getModifiedValue('reload'); + } + + /** + * Get the rate of fire for this module, taking in to account modifications + * @return {Number} the rate of fire for this module + */ + getRoF() { + return this._getModifiedValue('rof'); + } + + /** + * Get the facing limit for this module, taking in to account modifications + * @return {Number} the facing limit for this module + */ + getFacingLimit() { + return this._getModifiedValue('facinglimit'); + } + + /** + * Get the hull boost for this module, taking in to account modifications + * @return {Number} the hull boost for this module + */ + getHullBoost() { + return this._getModifiedValue('hullboost'); + } + +} diff --git a/src/app/shipyard/ModuleSet.js b/src/app/shipyard/ModuleSet.js index 04711cb8..89fb0b55 100755 --- a/src/app/shipyard/ModuleSet.js +++ b/src/app/shipyard/ModuleSet.js @@ -1,4 +1,4 @@ - +import Module from './Module'; import { BulkheadNames } from './Constants'; /** @@ -37,7 +37,7 @@ export default class ModuleSet { this.intClass = {}; this.bulkheads = shipData.bulkheads.map((b, i) => { - return Object.assign({ grp: 'bh', name: BulkheadNames[i], index: i, class: '', rating: '' }, b); + return Object.assign(new Module(), { grp: 'bh', id: i, name: BulkheadNames[i], index: i, class: '', rating: '' }, b); }); this.standard[0] = filter(stnd.pp, maxStandardArr[0], 0, mass); // Power Plant @@ -66,7 +66,7 @@ export default class ModuleSet { * @return {Object} Bulkhead module details */ getBulkhead(index) { - return this.bulkheads[index] || null; + return this.bulkheads[index] ? new Module({ template: this.bulkheads[index] }) : null; } /** @@ -126,11 +126,11 @@ export default class ModuleSet { let pd = this.standard[4][0]; for (let p of this.standard[4]) { - if (p.mass < pd.mass && p.enginecapacity >= boostEnergy) { + if (p.mass < pd.mass && p.engcap >= boostEnergy) { pd = p; } } - return pd; + return new Module({ template: pd }); }; /** @@ -146,7 +146,7 @@ export default class ModuleSet { th = t; } } - return th; + return new Module({ template: th }); }; /** @@ -162,7 +162,7 @@ export default class ModuleSet { sg = s; } } - return sg; + return new Module({ template: sg }); }; /** @@ -175,10 +175,10 @@ export default class ModuleSet { for (let p of this.standard[0]) { // Provides enough power, is lighter or the same mass as current power plant but better output/efficiency - if (p.pGen >= powerNeeded && (p.mass < pp.mass || (p.mass == pp.mass && p.pGen > pp.pGen))) { + if (p.pgen >= powerNeeded && (p.mass < pp.mass || (p.mass == pp.mass && p.pgen > pp.pgen))) { pp = p; } } - return pp; + return new Module({ template: pp }); } } diff --git a/src/app/shipyard/ModuleUtils.js b/src/app/shipyard/ModuleUtils.js index 4543bf32..dc0b95e6 100755 --- a/src/app/shipyard/ModuleUtils.js +++ b/src/app/shipyard/ModuleUtils.js @@ -1,7 +1,12 @@ import { ModuleNameToGroup, BulkheadNames, StandardArray } from './Constants'; import ModuleSet from './ModuleSet'; +import Module from './Module'; import { Ships, Modules } from 'coriolis-data/dist'; +/* + * All functions below must return a fresh Module rather than a definition or existing module, as + * the resultant object can be altered with modifications. + */ /** @@ -9,9 +14,45 @@ import { Ships, Modules } from 'coriolis-data/dist'; * @return {Object} Cargo hatch model */ export function cargoHatch() { - return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 }; + let hatch = new Module(); + Object.assign(hatch, { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 }); + return hatch; }; +/** + * Finds the module with the specific group and ID + * @param {String} grp Module group (pp - power plant, pl - pulse laser etc) + * @param {String} id The module ID + * @return {Object} The module or null + */ +export function findModule(grp, id) { + // See if it's a standard module + if (Modules.standard[grp]) { + let standardmod = Modules.standard[grp].find(e => e.id == id); + if (standardmod != null) { + return new Module({ template: standardmod }); + } + } + + // See if it's an internal module + if (Modules.internal[grp]) { + let internalmod = Modules.internal[grp].find(e => e.id == id); + if (internalmod != null) { + return new Module({ template: internalmod }); + } + } + + // See if it's a hardpoint module + if (Modules.hardpoints[grp]) { + let hardpointmod = Modules.hardpoints[grp].find(e => e.id == id); + if (hardpointmod != null) { + return new Module({ template: hardpointmod }); + } + } + + return null; +} + /** * Finds the standard module type with the specified ID * @param {String|Number} type Standard Module Type (0/pp - Power Plant, 1/t - Thrusters, etc) @@ -24,6 +65,9 @@ export function standard(type, id) { } let s = Modules.standard[type].find(e => e.id == id || (e.class == id.charAt(0) && e.rating == id.charAt(1))); + if (s) { + s = new Module({ template: s }); + } return s || null; }; @@ -37,7 +81,7 @@ export function hardpoints(id) { let group = Modules.hardpoints[n]; for (let i = 0; i < group.length; i++) { if (group[i].id == id) { - return group[i]; + return new Module({ template: group[i] }); } } } @@ -54,7 +98,7 @@ export function internal(id) { let group = Modules.internal[n]; for (let i = 0; i < group.length; i++) { if (group[i].id == id) { - return group[i]; + return new Module({ template: group[i] }); } } } diff --git a/src/app/shipyard/Serializer.js b/src/app/shipyard/Serializer.js index 99c47323..1fb3e477 100644 --- a/src/app/shipyard/Serializer.js +++ b/src/app/shipyard/Serializer.js @@ -24,6 +24,10 @@ function standardToSchema(standard) { o.name = standard.m.name; } + if (standard.m.mods && Object.keys(standard.m.mods).length > 0) { + o.modifications = standard.m.mods; + } + return o; } return null; @@ -53,6 +57,9 @@ function slotToSchema(slot) { if (slot.m.missile) { o.missile = slot.m.missile; } + if (slot.m.mods && Object.keys(slot.m.mods).length > 0) { + o.modifications = slot.m.mods; + } return o; } return null; @@ -71,12 +78,12 @@ export function toDetailedBuild(buildName, ship) { code = ship.toString(); let data = { - $schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#', + $schema: 'http://cdn.coriolis.io/schemas/ship-loadout/4.json#', name: buildName, ship: ship.name, references: [{ name: 'Coriolis.io', - url: `https://coriolis.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`, + url: `https://coriolis.edcd.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`, code, shipId: ship.id }], @@ -109,7 +116,7 @@ export function toDetailedBuild(buildName, ship) { }; /** - * Instantiates a ship from a ship-loadout object + * Instantiates a ship from a ship-loadout object, using the code * @param {Object} detailedBuild ship-loadout object * @return {Ship} Ship instance */ @@ -120,6 +127,26 @@ export function fromDetailedBuild(detailedBuild) { throw 'No such ship: ' + detailedBuild.ship; } + let comps = detailedBuild.components; + let stn = comps.standard; + let shipData = Ships[shipId]; + let ship = new Ship(shipId, shipData.properties, shipData.slots); + + return ship.buildFrom(detailedBuild.references[0].code); +}; + +/** + * Instantiates a ship from a ship-loadout object + * @param {Object} detailedBuild ship-loadout object + * @return {Ship} Ship instance + */ +export function oldfromDetailedBuild(detailedBuild) { + let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase()); + + if (!shipId) { + throw 'No such ship: ' + detailedBuild.ship; + } + let comps = detailedBuild.components; let stn = comps.standard; let priorities = [stn.cargoHatch && stn.cargoHatch.priority !== undefined ? stn.cargoHatch.priority - 1 : 0]; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 3b006005..25e35110 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1,14 +1,17 @@ -import { ArmourMultiplier } from './Constants'; import * as Calc from './Calculations'; import * as ModuleUtils from './ModuleUtils'; +import Module from './Module'; import LZString from 'lz-string'; +import isEqual from 'lodash/lang'; +import { Modifications } from 'coriolis-data/dist'; +const zlib = require('zlib'); const UNIQUE_MODULES = ['psg', 'sg', 'bsg', 'rf', 'fs', 'fh']; /** - * Returns the power usage type of a slot and it's particular modul + * Returns the power usage type of a slot and it's particular module * @param {Object} slot The Slot - * @param {Object} modul The modul in the slot + * @param {Object} modul The module in the slot * @return {String} The key for the power usage type */ function powerUsageType(slot, modul) { @@ -114,7 +117,7 @@ export default class Ship { */ canThrust() { return this.getSlotStatus(this.standard[1]) == 3 && // Thrusters are powered - this.ladenMass < this.standard[1].m.maxmass; // Max mass not exceeded + this.ladenMass < this.standard[1].m.getMaxMass(); // Max mass not exceeded } /** @@ -122,9 +125,9 @@ export default class Ship { * @return {[type]} True if boost capable */ canBoost() { - return this.canThrust() && // Thrusters operational - this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational - this.boostEnergy <= this.standard[4].m.enginecapacity; // PD capacitor is sufficient for boost + 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 } /** @@ -158,7 +161,8 @@ export default class Ship { */ calcUnladenRange(massDelta, fuel, fsd) { fsd = fsd || this.standard[2].m; - return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsd.maxfuel, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); + let fsdMaxFuelPerJump = fsd instanceof Module ? fsd.getMaxFuelPerJump() : fsd.maxfuel; + return Calc.jumpRange(this.unladenMass + (massDelta || 0) + Math.min(fsdMaxFuelPerJump, fuel || this.fuelCapacity), fsd || this.standard[2].m, fuel); } /** @@ -189,9 +193,11 @@ export default class Ship { * @return {Number} Recovery time in seconds */ calcShieldRecovery() { - if (this.shieldStrength && this.sgSlot) { + 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.shieldStrength / 2) / this.sgSlot.m.recover) + 15; + return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15; } return 0; } @@ -203,9 +209,11 @@ export default class Ship { * @return {Number} 50 - 100% Recharge time in seconds */ calcShieldRecharge() { - if (this.shieldStrength && this.sgSlot) { + 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.shieldStrength / 2) / (this.sgSlot.m.grp == 'bsg' ? 1.8 : 1); + return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate); } return 0; } @@ -214,16 +222,20 @@ export default class Ship { * Calculate the hypothetical shield strength for the ship using the specified parameters * @param {Object} sg [optional] Shield Generator to use * @param {Number} multiplierDelta [optional] Change to shield multiplier (+0.2, - 0.12, etc) - * @return {Number} Shield strength in MH + * @return {Number} Shield strength in MJ */ calcShieldStrengthWith(sg, multiplierDelta) { if (!sg) { - if (!this.sgSlot) { + let sgSlot = this.findInternalByGroup('sg'); + if (!sgSlot) { return 0; } - sg = this.sgSlot.m; + sg = sgSlot.m; } - return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, this.shieldMultiplier + (multiplierDelta || 0)); + + // 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)); } /** @@ -284,7 +296,9 @@ export default class Ship { '.', this.getPowerEnabledString(), '.', - this.getPowerPrioritesString() + this.getPowerPrioritiesString(), + '.', + this.getModificationsString() ].join(''); } @@ -324,6 +338,17 @@ export default class Ship { return this.serialized.hardpoints; } + /** + * Serializes the modifications to a string + * @return {String} Serialized modifications 'code' + */ + getModificationsString() { + // Modifications can be updated outside of the ship's direct knowledge, for example when sliders change the value, + // so always recreate it from scratch + this.updateModificationsString(); + return this.serialized.modifications; + } + /** * Get the serialized module active/inactive settings * @return {String} Serialized active/inactive settings @@ -336,7 +361,7 @@ export default class Ship { * Get the serialized module priority settings * @return {String} Serialized priority settings */ - getPowerPrioritesString() { + getPowerPrioritiesString() { return this.serialized.priorities; } @@ -368,18 +393,68 @@ export default class Ship { return this; } + /** + * 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 + */ + setModification(m, name, value) { + // Handle special cases + if (name == 'pgen') { + // Power generation + m.setModValue(name, value); + this.updatePowerGenerated(); + } else if (name == 'power') { + // Power usage + m.setModValue(name, value); + this.updatePowerUsed(); + } else if (name == 'mass') { + // Mass + let oldMass = m.getMass(); + m.setModValue(name, value); + let newMass = m.getMass(); + this.unladenMass = this.unladenMass - oldMass + newMass; + this.ladenMass = this.ladenMass - oldMass + newMass; + this.updateTopSpeed(); + this.updateJumpStats(); + } else if (name == 'maxfuel') { + m.setModValue(name, value); + this.updateJumpStats(); + } else if (name == 'optmass') { + m.setModValue(name, value); + // Could be for either thrusters or FSD + this.updateTopSpeed(); + this.updateJumpStats(); + } else if (name == 'optmul') { + m.setModValue(name, value); + // Could be for either thrusters or FSD + this.updateTopSpeed(); + this.updateJumpStats(); + } else if (name == 'shieldboost') { + m.setModValue(name, value); + this.updateShield(); + } else if (name == 'hullboost') { + m.setModValue(name, value); + this.updateArmour(); + } else { + // Generic + m.setModValue(name, value); + } + } + /** * Builds/Updates the ship instance with the ModuleUtils[comps] passed in. * @param {Object} comps Collection of ModuleUtils used to build the ship * @param {array} priorities Slot priorities * @param {Array} enabled Slot active/inactive + * @param {Array} mods Modifications * @return {this} The current ship instance for chaining */ - buildWith(comps, priorities, enabled) { + buildWith(comps, priorities, enabled, mods) { let internal = this.internal, standard = this.standard, hps = this.hardpoints, - bands = this.priorityBands, cl = standard.length, i, l; @@ -387,26 +462,26 @@ export default class Ship { this.fuelCapacity = 0; this.cargoCapacity = 0; this.ladenMass = 0; - this.armourAdded = 0; - this.armourMultiplier = 1; - this.shieldMultiplier = 1; + this.armour = this.baseArmour; + this.shield = this.baseShieldStrength; this.totalCost = this.m.incCost ? this.m.discountedCost : 0; this.unladenMass = this.hullMass; this.totalDps = 0; + this.totalEps = 0; + this.totalHps = 0; + this.shieldExplRes = 0; + this.shieldKinRes = 0; + this.shieldThermRes = 0; + this.hullExplRes = 0; + this.hullKinRes = 0; + this.hullThermRes = 0; this.bulkheads.m = null; this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); + this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; - - for (i = 0, l = this.priorityBands.length; i < l; i++) { - this.priorityBands[i].deployed = 0; - this.priorityBands[i].retracted = 0; - } - - if (this.cargoHatch.enabled) { - bands[this.cargoHatch.priority].retracted += this.cargoHatch.m.power; - } + this.cargoHatch.mods = mods ? mods[0] : {}; for (i = 0; i < cl; i++) { standard[i].cat = 0; @@ -415,9 +490,10 @@ export default class Ship { standard[i].type = 'SYS'; standard[i].m = null; // Resetting 'old' modul if there was one standard[i].discountedCost = 0; - if (comps) { - this.use(standard[i], ModuleUtils.standard(i, comps.standard[i]), true); + let module = ModuleUtils.standard(i, comps.standard[i]); + if (module != null) { module.mods = mods && mods[i + 1] ? mods[i + 1] : {}; } + this.use(standard[i], module, true); } } @@ -434,7 +510,9 @@ export default class Ship { hps[i].discountedCost = 0; if (comps && comps.hardpoints[i] !== 0) { - this.use(hps[i], ModuleUtils.hardpoints(comps.hardpoints[i]), true); + let module = ModuleUtils.hardpoints(comps.hardpoints[i]); + if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; } + this.use(hps[i], module, true); } } @@ -449,19 +527,23 @@ export default class Ship { internal[i].discountedCost = 0; if (comps && comps.internal[i] !== 0) { - this.use(internal[i], ModuleUtils.internal(comps.internal[i]), true); + let module = ModuleUtils.internal(comps.internal[i]); + if (module != null) { module.mods = mods && mods[cl + i] ? mods[cl + i] : {}; } + this.use(internal[i], module, true); } } // Update aggragated stats if (comps) { - this.updatePower() + this.updatePowerGenerated() + .updatePowerUsed() .updateJumpStats() - .updateShieldStrength() + .updateShield() + .updateArmour() .updateTopSpeed(); } - return this.updatePowerPrioritesString().updatePowerEnabledString(); + return this.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString(); } /** @@ -475,6 +557,7 @@ export default class Ship { let standard = new Array(this.standard.length), hardpoints = new Array(this.hardpoints.length), internal = new Array(this.internal.length), + mods = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length), parts = serializedString.split('.'), priorities = null, enabled = null, @@ -488,6 +571,19 @@ export default class Ship { priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split(''); } + if (parts[3]) { + const modstr = parts[3].replace(/-/g, '/'); + if (modstr.match(':')) { + this.decodeModificationsString(modstr, mods); + } else { + try { + this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(modstr, 'base64')), mods); + } catch (err) { + // Could be out-of-date URL; ignore + } + } + } + decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1))); return this.buildWith( @@ -498,7 +594,8 @@ export default class Ship { internal }, priorities, - enabled + enabled, + mods ); }; @@ -584,18 +681,20 @@ export default class Ship { if (slot.enabled != enabled) { // Enabled state is changing slot.enabled = enabled; if (slot.m) { - this.priorityBands[slot.priority][powerUsageType(slot, slot.m)] += enabled ? slot.m.power : -slot.m.power; - - if (ModuleUtils.isShieldGenerator(slot.m.grp)) { - this.updateShieldStrength(); - } else if (slot.m.grp == 'sb') { - this.shieldMultiplier += slot.m.shieldmul * (enabled ? 1 : -1); - this.updateShieldStrength(); - } else if (slot.m.dps) { - this.totalDps += slot.m.dps * (enabled ? 1 : -1); + if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp == 'sb') { + this.updateShield(); + } + 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); } - this.updatePower(); + this.updatePowerUsed(); this.updatePowerEnabledString(); } } @@ -615,10 +714,7 @@ export default class Ship { this.updatePowerPrioritesString(); if (slot.enabled) { // Only update power if the slot is enabled - let usage = powerUsageType(slot, slot.m); - this.priorityBands[oldPriority][usage] -= slot.m.power; - this.priorityBands[newPriority][usage] += slot.m.power; - this.updatePower(); + this.updatePowerUsed(); } return true; } @@ -634,7 +730,12 @@ export default class Ship { * @return {this} The ship instance (for chaining operations) */ updateStats(slot, n, old, preventUpdate) { - let powerChange = slot == this.standard[0]; + let powerGeneratedChange = slot == this.standard[0]; + let powerUsedChange = false; + + 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'); if (old) { // Old modul now being removed switch (old.grp) { @@ -644,27 +745,27 @@ export default class Ship { case 'cr': this.cargoCapacity -= old.cargo; break; - case 'hr': - this.armourAdded -= old.armouradd; - break; - case 'sb': - this.shieldMultiplier -= slot.enabled ? old.shieldmul : 0; - break; } if (slot.incCost && old.cost) { this.totalCost -= old.cost * this.moduleCostMultiplier; } - if (old.power && slot.enabled) { - this.priorityBands[slot.priority][powerUsageType(slot, old)] -= old.power; - powerChange = true; - - if (old.dps) { - this.totalDps -= old.dps; - } + if (old.getPowerUsage() > 0 && slot.enabled) { + powerUsedChange = true; } - this.unladenMass -= old.mass || 0; + + 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; } if (n) { @@ -675,12 +776,6 @@ export default class Ship { case 'cr': this.cargoCapacity += n.cargo; break; - case 'hr': - this.armourAdded += n.armouradd; - break; - case 'sb': - this.shieldMultiplier += slot.enabled ? n.shieldmul : 0; - break; } if (slot.incCost && n.cost) { @@ -688,49 +783,105 @@ export default class Ship { } if (n.power && slot.enabled) { - this.priorityBands[slot.priority][powerUsageType(slot, n)] += n.power; - powerChange = true; - - if (n.dps) { - this.totalDps += n.dps; - } + powerUsedChange = true; } - this.unladenMass += n.mass || 0; + + 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; - this.armour = this.armourAdded + Math.round(this.baseArmour * this.armourMultiplier); if (!preventUpdate) { - if (powerChange) { - this.updatePower(); + if (powerGeneratedChange) { + this.updatePowerGenerated(); + } + if (powerUsedChange) { + this.updatePowerUsed(); + } + if (armourChange) { + this.updateArmour(); + } + if (shieldChange) { + this.updateShield(); } this.updateTopSpeed(); this.updateJumpStats(); - this.updateShieldStrength(); } return this; } /** - * Update all power calculations + * Update power calculations when amount generated changes * @return {this} The ship instance (for chaining operations) */ - updatePower() { - let bands = this.priorityBands; - let prevRetracted = 0, prevDeployed = 0; + updatePowerGenerated() { + this.powerAvailable = this.standard[0].m.getPowerGeneration(); + return this; + }; + /** + * Update power calculations when amount consumed changes + * @return {this} The ship instance (for chaining operations) + */ + updatePowerUsed() { + let bands = [ + { deployed: 0, retracted: 0, }, + { deployed: 0, retracted: 0, }, + { deployed: 0, retracted: 0, }, + { deployed: 0, retracted: 0, }, + { deployed: 0, retracted: 0, } + ]; + + if (this.cargoHatch.enabled) { + bands[this.cargoHatch.priority].retracted += this.cargoHatch.m.getPowerUsage(); + } + + for (let slotNum in this.standard) { + const slot = this.standard[slotNum]; + if (slot.m && slot.enabled) { + bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage(); + } + } + + for (let slotNum in this.internal) { + const slot = this.internal[slotNum]; + if (slot.m && slot.enabled) { + bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage(); + } + } + + for (let slotNum in this.hardpoints) { + const slot = this.hardpoints[slotNum]; + if (slot.m && slot.enabled) { + bands[slot.priority][powerUsageType(slot, slot.m)] += slot.m.getPowerUsage(); + } + } + + // Work out the running totals + let prevRetracted = 0, prevDeployed = 0; for (let i = 0, l = bands.length; i < l; i++) { let band = bands[i]; prevRetracted = band.retractedSum = prevRetracted + band.retracted; prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted; } - this.powerAvailable = this.standard[0].m.pGen; + // Update global stats this.powerRetracted = prevRetracted; this.powerDeployed = prevDeployed; + this.priorityBands = bands; + return this; - }; + } /** * Update top speed and boost @@ -744,12 +895,47 @@ export default class Ship { } /** - * Update Shield strength + * Update shield * @return {this} The ship instance (for chaining operations) */ - updateShieldStrength() { - this.sgSlot = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any - this.shieldStrength = this.sgSlot && this.sgSlot.enabled ? Calc.shieldStrength(this.hullMass, this.baseShieldStrength, this.sgSlot.m, this.shieldMultiplier) : 0; + updateShield() { + // Base shield from generator + let baseShield = 0; + let sgSlot = this.findInternalByGroup('sg'); + if (sgSlot && sgSlot.enabled) { + baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); + } + + let shield = baseShield; + + // Shield from boosters + for (let slot of this.hardpoints) { + if (slot.m && slot.m.grp == 'sb') { + shield += baseShield * slot.m.getShieldBoost(); + } + } + this.shield = shield; + return this; + } + + /** + * Update armour + * @return {this} The ship instance (for chaining operations) + */ + updateArmour() { + // Armour from bulkheads + let bulkhead = this.bulkheads.m; + let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost()); + + // 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'); + } + } + this.armour = armour; return this; } @@ -759,7 +945,7 @@ export default class Ship { */ updateJumpStats() { let fsd = this.standard[2].m; // Frame Shift Drive; - let { unladenMass, ladenMass, fuelCapacity } = this; + let { unladenMass, fuelCapacity } = this; this.unladenRange = this.calcUnladenRange(); // Includes fuel weight for jump this.fullTankRange = Calc.jumpRange(unladenMass + fuelCapacity, fsd); // Full Tank this.ladenRange = this.calcLadenRange(); // Includes full tank and caro @@ -811,16 +997,206 @@ export default class Ship { return this; } + /** + * Update the modifications string + * @return {this} The ship instance (for chaining operations) + */ + oldupdateModificationsString() { + let allMods = new Array(); + + 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)); + } + } + allMods.push(bulkheadMods.join(';')); + + for (let slot of this.standard) { + 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)); + } + } + allMods.push(slotMods.join(';')); + } + for (let slot of this.hardpoints) { + 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)); + } + } + allMods.push(slotMods.join(';')); + } + for (let slot of this.internal) { + 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)); + } + } + allMods.push(slotMods.join(';')); + } + this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, '')).replace(/\//g, '-'); + return this; + } + + /** + * Populate the modifications array with modification values from the code + * @param {String} code Serialized modification code + * @param {Array} arr Modification array + */ + decodeModificationsString(code, arr) { + let moduleMods = code.split(','); + for (let i = 0; i < arr.length; i++) { + arr[i] = {}; + if (moduleMods.length > i && moduleMods[i] != '') { + let mods = moduleMods[i].split(';'); + 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]); + } else { + arr[i][modElements[0]] = Number(modElements[1]); + } + } + } + } + } + + /** + * Update the modifications string. + * 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 + * for the next module, and the next, and is terminated by a slot ID of -1. + * @return {this} The ship instance (for chaining operations) + */ + updateModificationsString() { + // Start off by gathering the information that we need + let slots = new Array(); + + 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) }); + } + } + slots.push(bulkheadMods); + + for (let slot of this.standard) { + 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) }); + } + } + slots.push(slotMods); + } + + for (let slot of this.hardpoints) { + 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) }); + } + } + slots.push(slotMods); + } + + for (let slot of this.internal) { + 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) }); + } + } + slots.push(slotMods); + } + + // Now work out the size of the binary buffer from our modifications array + let bufsize = 0; + for (let slot of slots) { + if (slot.length > 0) { + bufsize = bufsize + 1 + (5 * slot.length) + 1; + } + } + + if (bufsize > 0) { + bufsize = bufsize + 1; // For end marker + // Now create and populate the buffer + let buffer = Buffer.alloc(bufsize); + let curpos = 0; + let i = 0; + for (let slot of slots) { + if (slot.length > 0) { + buffer.writeInt8(i, curpos++); + for (let slotMod of slot) { + buffer.writeInt8(slotMod.id, curpos++); + buffer.writeInt32LE(slotMod.value, curpos); + curpos += 4; + } + buffer.writeInt8(-1, curpos++); + } + i++; + } + if (curpos > 0) { + buffer.writeInt8(-1, curpos++); + } + + this.serialized.modifications = zlib.gzipSync(buffer).toString('base64').replace(/\//g, '-'); + } else { + this.serialized.modifications = null; + } + return this; + } + + /** + * Populate the modifications array with modification values from the code. + * See updateModificationsString() for details of the structure. + * @param {String} buffer Buffer holding modification info + * @param {Array} arr Modification array + */ + decodeModificationsStruct(buffer, arr) { + let curpos = 0; + let slot = buffer.readInt8(curpos++); + while (slot != -1) { + let modifications = {}; + let modificationId = buffer.readInt8(curpos++); + while (modificationId != -1) { + let modificationValue = buffer.readInt32LE(curpos); + curpos += 4; + modifications[Modifications.modifiers[modificationId]] = modificationValue; + modificationId = buffer.readInt8(curpos++); + } + arr[slot] = modifications; + slot = buffer.readInt8(curpos++); + } + } + /** * Update a slot with a the modul if the id is different from the current id for this slot. * Has logic handling ModuleUtils that you may only have 1 of (Shield Generator or Refinery). * * @param {Object} slot The modul slot - * @param {Object} m Properties for the selected module + * @param {Object} mdef Properties for the selected modul * @param {boolean} preventUpdate If true, do not update aggregated stats * @return {this} The ship instance (for chaining operations) */ - use(slot, m, preventUpdate) { + use(slot, mdef, preventUpdate) { + // See if the module passed in is really a module or just a definition, and fix it accordingly so that we have a module instance + let m; + if (mdef == null) { + m = null; + } else if (mdef instanceof Module) { + m = mdef; + } else { + m = new Module({ grp: mdef.grp, id: mdef.id }); + } + if (slot.m != m) { // Selecting a different modul // Slot is an internal slot, is not being emptied, and the selected modul group/type must be of unique if (slot.cat == 2 && m && UNIQUE_MODULES.indexOf(m.grp) != -1) { @@ -843,6 +1219,7 @@ export default class Ship { case 1: this.serialized.hardpoints = null; break; case 2: this.serialized.internal = null; } + this.serialized.modifications = null; } return this; } @@ -857,7 +1234,6 @@ export default class Ship { let oldBulkhead = this.bulkheads.m; this.bulkheads.m = this.availCS.getBulkhead(index); this.bulkheads.discountedCost = this.bulkheads.m.cost * this.moduleCostMultiplier; - this.armourMultiplier = ArmourMultiplier[index]; this.updateStats(this.bulkheads, this.bulkheads.m, oldBulkhead, preventUpdate); this.serialized.standard = null; return this; @@ -907,14 +1283,14 @@ export default class Ship { updated = false; // Find lightest Thruster that still works for the ship at max mass let th = m.th ? ModuleUtils.standard(1, m.th) : this.availCS.lightestThruster(this.ladenMass); - if (th !== standard[1].m) { + if (!isEqual.isEqual(th, standard[1].m)) { this.use(standard[1], th); updated = true; } // Find lightest Power plant that can power the ship let pp = m.pp ? ModuleUtils.standard(0, m.pp) : this.availCS.lightestPowerPlant(Math.max(this.powerRetracted, this.powerDeployed), m.ppRating); - if (pp !== standard[0].m) { + if (!isEqual.isEqual(pp, standard[0].m)) { this.use(standard[0], pp); updated = true; } @@ -965,5 +1341,4 @@ export default class Ship { } return this; } - } diff --git a/src/app/shipyard/ShipRoles.js b/src/app/shipyard/ShipRoles.js index fc06fbe0..42b03ee6 100644 --- a/src/app/shipyard/ShipRoles.js +++ b/src/app/shipyard/ShipRoles.js @@ -115,7 +115,7 @@ export function explorer(ship, planetary) { if (sgSlot) { // The SG and Fuel scoop to not need to be powered at the same time - if (sgSlot.m.power > fuelScoopSlot.m.power) { // The Shield generator uses the most power + if (sgSlot.m.getPowerUsage() > fuelScoopSlot.m.getPowerUsage()) { // The Shield generator uses the most power ship.setSlotEnabled(fuelScoopSlot, false); } else { // The Fuel scoop uses the most power ship.setSlotEnabled(sgSlot, false); diff --git a/src/app/utils/SlotFunctions.js b/src/app/utils/SlotFunctions.js index fa63852c..74697c7f 100644 --- a/src/app/utils/SlotFunctions.js +++ b/src/app/utils/SlotFunctions.js @@ -1,6 +1,7 @@ import React from 'react'; import cn from 'classnames'; import { isShieldGenerator } from '../shipyard/ModuleUtils'; +import Module from '../shipyard/Module'; import { Infinite } from '../components/SvgIcons'; import Persist from '../stores/Persist'; @@ -117,17 +118,15 @@ const PROP_BLACKLIST = { ssdam: 1, mjdps: 1, mjeps: 1, - M: 1, - P: 1, mass: 1, cost: 1, recover: 1, - weaponcapacity: 1, - weaponrecharge: 1, - enginecapacity: 1, - enginerecharge: 1, - systemcapacity: 1, - systemrecharge: 1, + wepcap: 1, + weprate: 1, + engcap: 1, + engrate: 1, + syscap: 1, + sysrate: 1, breachdps: 1, breachmin: 1, breachmax: 1, @@ -135,16 +134,14 @@ const PROP_BLACKLIST = { }; const TERM_LOOKUP = { - pGen: 'power', + pgen: 'power', armouradd: 'armour', - shieldmul: 'multiplier', rof: 'ROF', dps: 'DPS' }; const FORMAT_LOOKUP = { - time: 'time', - shieldmul: 'rPct' + time: 'time' }; const UNIT_LOOKUP = { @@ -155,7 +152,7 @@ const UNIT_LOOKUP = { recharge: 'MJ', rangeLS: 'Ls', power: 'MJ', - pGen: 'MJ', + pgen: 'MJ', rof: 'ps' }; @@ -206,42 +203,54 @@ function diff(format, mVal, mmVal) { * @return {React.Component} Component to be rendered */ export function diffDetails(language, m, mm) { - mm = mm || {}; let { formats, translate, units } = language; let propDiffs = []; + + let mCost = m.cost || 0; + let mmCost = mm ? mm.cost : 0; + if (mCost != mmCost) propDiffs.push(
    {translate('cost')}: {mCost ? Math.round(mCost * (1 - Persist.getModuleDiscount())) : 0}{units.CR}
    ); + let mMass = m.mass || 0; - let mmMass = mm.mass || 0; - let massDiff = mMass - mmMass; - let capDiff = (m.fuel || m.cargo || 0) - (mm.fuel || mm.cargo || 0); + let mmMass = mm ? mm.getMass() : 0; + if (mMass != mmMass) propDiffs.push(
    {translate('mass')}: {diff(formats.round, mMass, mmMass)}{units.T}
    ); + + let mPowerUsage = m.power || 0; + let mmPowerUsage = mm ? mm.getPowerUsage() : 0; + if (mPowerUsage != mmPowerUsage) propDiffs.push(
    {translate('power')}: {diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MJ}
    ); + +// for (let p in m) { +// if (!PROP_BLACKLIST[p] && !isNaN(m[p])) { +// let mVal = m[p] === null ? Infinity : m[p]; +// let mmVal = mm[p] === null ? Infinity : mm[p]; +// let format = formats[FORMAT_LOOKUP[p]] || formats.round; +// propDiffs.push(
    +// {`${translate(TERM_LOOKUP[p] || p)}: `} +// {diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]} +//
    ); +// } +// } + + let mDps = m.damage * (m.rpshot || 1) * m.rof || 0; + let mmDps = mm ? mm.getDps() || 0 : 0; + if (mDps != mmDps) propDiffs.push(
    {translate('dps')}: {diff(formats.round, mDps, mmDps)}
    ); + let mAffectsShield = isShieldGenerator(m.grp) || m.grp == 'sb'; - let mmAffectsShield = isShieldGenerator(mm.grp) || mm.grp == 'sb'; - - propDiffs.push(
    {translate('cost')}: {m.cost ? Math.round(m.cost * (1 - Persist.getModuleDiscount())) : 0}{units.CR}
    ); - propDiffs.push(
    {translate('mass')}: {diff(formats.round, mMass, mmMass)}{units.T}
    ); - - for (let p in m) { - if (!PROP_BLACKLIST[p] && !isNaN(m[p])) { - let mVal = m[p] === null ? Infinity : m[p]; - let mmVal = mm[p] === null ? Infinity : mm[p]; - let format = formats[FORMAT_LOOKUP[p]] || formats.round; - propDiffs.push(
    - {`${translate(TERM_LOOKUP[p] || p)}: `} - {diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]} -
    ); - } - } - + let mmAffectsShield = isShieldGenerator(mm ? mm.grp : null) || mm && mm.grp == 'sb'; if (mAffectsShield || mmAffectsShield) { let shield = this.calcShieldStrengthWith(); // Get shield strength regardless of slot active / inactive let newShield = 0; if (mAffectsShield) { if (m.grp == 'sb') { // Both m and mm must be utility modules if this is true - newShield = this.calcShieldStrengthWith(null, m.shieldmul - (mm.shieldmul || 0)); + newShield = this.calcShieldStrengthWith(null, m.shieldboost - (mm ? mm.getShieldBoost() || 0 : 0)); } else { newShield = this.calcShieldStrengthWith(m); } + } else { + // Old module must be a shield booster + newShield = this.calcShieldStrengthWith(null, -mm.getShieldBoost()); } + let sgDiffClass = Math.round((newShield - shield) * 100) / 100 == 0 ? 'muted' : (newShield > shield ? 'secondary' : 'warning'); propDiffs.push(
    {translate('shields')}: {diff(formats.int, newShield, shield)}{units.MJ}
    ); @@ -250,24 +259,28 @@ export function diffDetails(language, m, mm) { if (m.grp == 'pd') { propDiffs.push(
    {`${translate('WEP')}: `} - {m.weaponcapacity}{units.MJ} + {m.wepcap}{units.MJ} {' / '} - {m.weaponrecharge}{units.MW} + {m.weprate}{units.MW}
    ); propDiffs.push(
    {`${translate('SYS')}: `} - {m.systemcapacity}{units.MJ} + {m.syscap}{units.MJ} {' / '} - {m.systemrecharge}{units.MW} + {m.sysrate}{units.MW}
    ); propDiffs.push(
    {`${translate('ENG')}: `} - {m.enginecapacity}{units.MJ} + {m.engcap}{units.MJ} {' / '} - {m.enginerecharge}{units.MW} + {m.engrate}{units.MW}
    ); } + let massDiff = mMass - mmMass; + let mCap = m.fuel || m.cargo || 0; + let mmCap = mm ? mm.fuel || mm.cargo || 0 : 0; + let capDiff = mCap - mmCap; if (m.grp == 'fsd' || massDiff || capDiff) { let fsd = m.grp == 'fsd' ? m : null; let maxRange = this.calcUnladenRange(massDiff, m.fuel, fsd); @@ -281,5 +294,5 @@ export function diffDetails(language, m, mm) { } } - return
    {propDiffs}
    ; + return propDiffs ?
    {propDiffs}
    : null; } diff --git a/src/less/app.less b/src/less/app.less index b4a5443f..7f4564ec 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -163,4 +163,4 @@ footer { float: right; text-align: right; } -} \ No newline at end of file +}
    {translate('size')}{translate('MNV')} {translate('speed')} {translate('boost')} {translate('DPS')}{translate('EPS')}{translate('HPS')} {translate('armour')} {translate('shields')} {translate('mass')}
    {translate(SizeMap[ship.class])}{ship.agility}/10 { ship.canThrust() ? {int(ship.topSpeed)} {u['m/s']} : 0 } { ship.canBoost() ? {int(ship.topBoost)} {u['m/s']} : 0 }{round(ship.totalDps)}{int(ship.armour)} {armourDetails}{int(ship.shieldStrength)} {u.MJ} { ship.shieldMultiplier > 1 && ship.shieldStrength > 0 ? ({formats.rPct(ship.shieldMultiplier)}) : null }{f1(ship.totalDps)}{f1(ship.totalEps)}{f1(ship.totalHps)}{int(ship.armour)}{int(ship.shield)} {u.MJ} {sgRecover} {sgRecharge} {ship.hullMass} {u.T}{round(ship.unladenMass)} {u.T}{round(ship.ladenMass)} {u.T}{int(ship.unladenMass)} {u.T}{int(ship.ladenMass)} {u.T} {round(ship.cargoCapacity)} {u.T} {round(ship.fuelCapacity)} {u.T}{round(ship.unladenRange)} {u.LY}{round(ship.fullTankRange)} {u.LY}{round(ship.ladenRange)} {u.LY}{round(ship.maxJumpCount)}{round(ship.unladenFastestRange)} {u.LY}{round(ship.ladenFastestRange)} {u.LY}{f2(ship.unladenRange)} {u.LY}{f2(ship.fullTankRange)} {u.LY}{f2(ship.ladenRange)} {u.LY}{int(ship.maxJumpCount)}{f2(ship.unladenFastestRange)} {u.LY}{f2(ship.ladenFastestRange)} {u.LY} {ship.masslock}