diff --git a/.travis.yml b/.travis.yml
index 89bc3d76..49fe7b3e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,7 +8,8 @@ cache:
directories:
- node_modules
-before_script:
+before_install:
+ - git clone https://github.com/EDCD/coriolis-data.git ../coriolis-data
script:
- npm run lint
diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx
index 2906a736..a31960cd 100644
--- a/src/app/components/AvailableModulesMenu.jsx
+++ b/src/app/components/AvailableModulesMenu.jsx
@@ -1,504 +1,504 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as ModuleUtils from '../shipyard/ModuleUtils';
-import TranslatedComponent from './TranslatedComponent';
-import { stopCtxPropagation } from '../utils/UtilityFunctions';
-import cn from 'classnames';
-import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
-
-const PRESS_THRESHOLD = 500; // mouse/touch down threshold
-
-/*
- * Categorisation of module groups
- */
-const GRPCAT = {
- 'sg': 'shields',
- 'bsg': 'shields',
- 'psg': 'shields',
- 'scb': 'shields',
- 'cc': 'limpet controllers',
- 'fx': 'limpet controllers',
- 'hb': 'limpet controllers',
- 'pc': 'limpet controllers',
- 'rpl': 'limpet controllers',
- 'pce': 'passenger cabins',
- 'pci': 'passenger cabins',
- 'pcm': 'passenger cabins',
- 'pcq': 'passenger cabins',
- 'fh': 'hangars',
- 'pv': 'hangars',
- 'fs': 'fuel',
- 'ft': 'fuel',
- 'hr': 'structural reinforcement',
- 'mrp': 'structural reinforcement',
- 'bl': 'lasers',
- 'pl': 'lasers',
- 'ul': 'lasers',
- 'ml': 'lasers',
- 'c': 'projectiles',
- 'mc': 'projectiles',
- 'axmc': 'experimental',
- 'fc': 'projectiles',
- 'rfl': 'experimental',
- 'pa': 'projectiles',
- 'rg': 'projectiles',
- 'mr': 'ordnance',
- 'axmr': 'experimental',
- 'rcpl': 'experimental',
- 'tp': 'ordnance',
- 'nl': 'ordnance',
- 'sc': 'scanners',
- 'ss': 'scanners',
- // Utilities
- 'cs': 'scanners',
- 'kw': 'scanners',
- 'ws': 'scanners',
- 'xs': 'scanners',
- 'ch': 'defence',
- 'po': 'defence',
- 'ec': 'defence',
- 'sfn': 'defence',
- // Standard
- 'gpp': 'guardian',
- 'gpc': 'guardian',
- 'ggc': 'guardian'
-};
-// Order here is the order in which items will be shown in the modules menu
-const CATEGORIES = {
- // Internals
- 'am': ['am'],
- 'cr': ['cr'],
- 'fi': ['fi'],
- 'fuel': ['ft', 'fs'],
- 'hangars': ['fh', 'pv'],
- 'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'],
- 'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
- 'rf': ['rf'],
- 'shields': ['sg', 'bsg', 'psg', 'scb'],
- 'structural reinforcement': ['hr', 'mrp'],
- 'dc': ['dc'],
- // Hardpoints
- 'lasers': ['pl', 'ul', 'bl', 'ml'],
- 'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
- 'ordnance': ['mr', 'tp', 'nl'],
- // Utilities
- 'sb': ['sb'],
- 'hs': ['hs'],
- 'defence': ['ch', 'po', 'ec'],
- 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
- // Experimental
- 'experimental': ['axmc', 'axmr', 'rfl', 'xs', 'sfn', 'rcpl'],
-
- // Guardian
- 'guardian': ['gpp', 'gpc', 'ggc']
-};
-
-/**
- * Available modules menu
- */
-export default class AvailableModulesMenu extends TranslatedComponent {
-
- static propTypes = {
- modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
- onSelect: PropTypes.func.isRequired,
- diffDetails: PropTypes.func,
- m: PropTypes.object,
- shipMass: PropTypes.number,
- warning: PropTypes.func,
- firstSlotId: PropTypes.string,
- lastSlotId: PropTypes.string,
- activeSlotId: PropTypes.string,
- slotDiv: PropTypes.object
- };
-
- static defaultProps = {
- shipMass: 0
- };
-
- /**
- * Constructor
- * @param {Object} props React Component properties
- * @param {Object} context React Component context
- */
- constructor(props, context) {
- super(props);
- this._hideDiff = this._hideDiff.bind(this);
- this.state = this._initState(props, context);
- this.slotItems = [];// Array to hold
refs.
- }
-
- /**
- * Initiate the list of available moduels
- * @param {Object} props React Component properties
- * @param {Object} context React Component context
- * @return {Object} list: Array of React Components, currentGroup Component if any
- */
- _initState(props, context) {
- let translate = context.language.translate;
- let { m, warning, shipMass, onSelect, modules, firstSlotId, lastSlotId } = props;
- let list, currentGroup;
-
- let buildGroup = this._buildGroup.bind(
- this,
- translate,
- m,
- warning,
- shipMass - (m && m.mass ? m.mass : 0),
- (m, event) => {
- this._hideDiff(event);
- onSelect(m);
- }
- );
-
- if (modules instanceof Array) {
- list = buildGroup(modules[0].grp, modules);
- } else {
- list = [];
- // At present time slots with grouped options (Hardpoints and Internal) can be empty
- if (m) {
- let emptyId = 'empty';
- if(this.firstSlotId == null) this.firstSlotId = emptyId;
- let keyDown = this._keyDown.bind(this, onSelect);
- list.push( this.slotItems[emptyId] = slotItem} >{translate('empty')}
);
- }
-
- // Need to regroup the modules by our own categorisation
- let catmodules = {};
- // Pre-create to preserve ordering
- for (let cat in CATEGORIES) {
- catmodules[cat] = [];
- }
- for (let g in modules) {
- const moduleCategory = GRPCAT[g] || g;
- const existing = catmodules[moduleCategory] || [];
- catmodules[moduleCategory] = existing.concat(modules[g]);
- }
-
- for (let category in catmodules) {
- let categoryHeader = false;
- // Order through CATEGORIES if present
- const categories = CATEGORIES[category] || [category];
- if (categories && categories.length) {
- for (let n in categories) {
- const grp = categories[n];
- // We now have the group and the category. We might not have any modules, though...
- if (modules[grp]) {
- // Decide if we need a category header as well as a group header
- if (categories.length === 1) {
- // Show category header instead of group header
- if (m && grp == m.grp) {
- list.push( this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}
);
- } else {
- list.push({translate(category)}
);
- }
- } else {
- // Show category header as well as group header
- if (!categoryHeader) {
- list.push({translate(category)}
);
- categoryHeader = true;
- }
- if (m && grp == m.grp) {
- list.push( this.groupElem = elem} key={grp} className={'select-group cap'}>{translate(grp)}
);
- } else {
- list.push({translate(grp)}
);
- }
- }
- list.push(buildGroup(grp, modules[grp]));
- }
- }
- }
- }
- }
- let trackingFocus = false;
- return { list, currentGroup, trackingFocus };
- }
-
- /**
- * Generate React Components for Module Group
- * @param {Function} translate Translate function
- * @param {Object} mountedModule Mounted Module
- * @param {Function} warningFunc Warning function
- * @param {number} mass Mass
- * @param {function} onSelect Select/Mount callback
- * @param {string} grp Group name
- * @param {Array} modules Available modules
- * @return {*} Available Module Group contents
- * @param {*} firstSlotId ID of the first slot
- * @param {*} lastSlotId ID of the last slot
- */
- _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules, firstSlotId, lastSlotId) {
- let prevClass = null, prevRating = null, prevName;
- let elems = [];
-
- const sortedModules = modules.sort(this._moduleOrder);
-
-
- // Calculate the number of items per class. Used so we don't have long lists with only a few items in each row
- const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => { count[cls] = ++count[cls] || 1; return count; }, {});
- const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
-
- let itemsOnThisRow = 0;
- for (let i = 0; i < sortedModules.length; i++) {
- let m = sortedModules[i];
- let mount = null;
- let disabled = false;
- prevName = m.name;
- if (ModuleUtils.isShieldGenerator(m.grp)) {
- // Shield generators care about maximum hull mass
- disabled = mass > m.maxmass;
- } else if (m.maxmass) {
- // Thrusters care about total mass
- disabled = mass + m.mass > m.maxmass;
- }
- let active = mountedModule && mountedModule.id === m.id;
- let classes = cn(m.name ? 'lc' : 'c', {
- warning: !disabled && warningFunc && warningFunc(m),
- active,
- disabled
- });
- let eventHandlers;
-
- if (disabled) {
- eventHandlers = {
- onKeyDown: this._keyDown.bind(this, null),
- onKeyUp: this._keyUp.bind(this, null)
-
- };
- } else {
- /**
- * Get the ids of the first and last elements in the that are focusable (i.e. are not active or disabled)
- * Will be used to keep focus inside the on Tab and Shift-Tab while it is visible
- */
- if (this.firstSlotId == null) this.firstSlotId = sortedModules[i].id;
- if (active) this.activeSlotId = sortedModules[i].id;
- this.lastSlotId = sortedModules[i].id;
-
- let showDiff = this._showDiff.bind(this, mountedModule, m);
- let select = onSelect.bind(null, m);
-
- eventHandlers = {
- onMouseEnter: this._over.bind(this, showDiff),
- onTouchStart: this._touchStart.bind(this, showDiff),
- onTouchEnd: this._touchEnd.bind(this, select),
- onMouseLeave: this._hideDiff,
- onClick: select,
- onKeyDown: this._keyDown.bind(this, select),
- onKeyUp: this._keyUp.bind(this, select)
- };
- }
-
- switch(m.mount) {
- case 'F': mount = ; break;
- case 'G': mount = ; break;
- case 'T': mount = ; break;
- }
- if (m.name && m.name === prevName) {
- // elems.push( );
- itemsOnThisRow = 0;
- }
- if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
- elems.push( );
- itemsOnThisRow = 0;
- }
- let tbIdx = (classes.indexOf('disabled') < 0) ? 0 : undefined;
- elems.push(
- this.slotItems[m.id] = slotItem}>
- {mount}
- {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
-
- );
-
- itemsOnThisRow++;
- prevClass = m.class;
- prevRating = m.rating;
- prevName = m.name;
- }
- return ;
- }
-
- /**
- * Generate tooltip content for the difference between the
- * mounted module and the hovered modules
- * @param {Object} mm The module mounet currently
- * @param {Object} m The hovered module
- * @param {DOMRect} rect DOMRect for target element
- */
- _showDiff(mm, m, rect) {
- if (this.props.diffDetails) {
- this.touchTimeout = null;
- this.context.tooltip(this.props.diffDetails(m, mm), rect);
- }
- }
-
- /**
- * Mouse over diff handler
- * @param {Function} showDiff diff tooltip callback
- * @param {SyntheticEvent} event Event
- */
- _over(showDiff, event) {
- event.preventDefault();
- showDiff(event.currentTarget.getBoundingClientRect());
- }
-
- /**
- * Toucch Start - Show diff after press, otherwise treat as tap
- * @param {Function} showDiff diff tooltip callback
- * @param {SyntheticEvent} event Event
- */
- _touchStart(showDiff, event) {
- event.preventDefault();
- let rect = event.currentTarget.getBoundingClientRect();
- this.touchTimeout = setTimeout(showDiff.bind(this, rect), PRESS_THRESHOLD);
- }
-
- /**
- * Touch End - Select module on tap
- * @param {Function} select Select module callback
- * @param {SyntheticEvent} event Event
- */
- _touchEnd(select, event) {
- event.preventDefault();
- if (this.touchTimeout !== null) { // If timeout has not fired (been nulled out) yet
- select();
- }
- this._hideDiff();
- }
-
- /**
- * Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
- * @param {Function} select Select module callback
- * @param {SyntheticEvent} event Event
- */
- _keyDown(select, event) {
- let className = event.currentTarget.attributes['class'].value;
- if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
- select();
- return;
- }
- let elemId = event.currentTarget.attributes['data-id'].value;
- if (className.indexOf('disabled') < 0 && event.key == 'Tab') {
- if (event.shiftKey && elemId == this.firstSlotId) {
- event.preventDefault();
- this.slotItems[this.lastSlotId].focus();
- return;
- }
- if (!event.shiftKey && elemId == this.lastSlotId) {
- event.preventDefault();
- this.slotItems[this.firstSlotId].focus();
- return;
- }
- }
- }
-
- /**
- * Key Up
- * @param {Function} select Select function
- * @param {SyntheticEvent} event Event
- */
- _keyUp(select,event) {
- // nothing here yet
- }
-
- /**
- * Hide diff tooltip
- * @param {SyntheticEvent} event Event
- */
- _hideDiff(event) {
- clearTimeout(this.touchTimeout);
- this.touchTimeout = null;
- this.context.tooltip();
- }
-
- /**
- * Order two modules suitably for display in module selection
- * @param {Object} a the first module
- * @param {Object} b the second module
- * @return {int} -1 if the first module should go first, 1 if the second module should go first
- */
- _moduleOrder(a, b) {
- // Named modules go last
- if (!a.name && b.name) {
- return -1;
- }
- if (a.name && !b.name) {
- return 1;
- }
- // Class ordered from highest (8) to lowest (1)
- if (a.class < b.class) {
- return 1;
- }
- if (a.class > b.class) {
- return -1;
- }
- // Mount type, if applicable
- if (a.mount && b.mount && a.mount !== b.mount) {
- if (a.mount === 'F' || (a.mount === 'G' && b.mount === 'T')) {
- return -1;
- } else {
- return 1;
- }
- }
- // Rating ordered from highest (A) to lowest (E)
- if (a.rating < b.rating) {
- return -1;
- }
- if (a.rating > b.rating) {
- return 1;
- }
- // Do not attempt to order by name at this point, as that mucks up the order of armour
- return 0;
- }
-
- /**
- * Scroll to mounted (if it exists) module group on mount
- */
- componentDidMount() {
- if (this.groupElem) { // Scroll to currently selected group
- this.node.scrollTop = this.groupElem.offsetTop;
- }
- /**
- * Set focus on active or first slot element, if applicable.
- */
- if (this.slotItems[this.activeSlotId]) {
- this.slotItems[this.activeSlotId].focus();
- } else if (this.slotItems[this.firstSlotId]) {
- this.slotItems[this.firstSlotId].focus();
- }
- }
- /**
- * Set focus to slot element ref (if we have one) after modules component unmounts
- */
- componentWillUnmount() {
- if(this.props.slotDiv) {
- this.props.slotDiv.focus();
- }
- }
-
- /**
- * Update state based on property and context changes
- * @param {Object} nextProps Incoming/Next properties
- * @param {Object} nextContext Incoming/Next conext
- */
- componentWillReceiveProps(nextProps, nextContext) {
- this.setState(this._initState(nextProps, nextContext));
- }
-
- /**
- * Render the list
- * @return {React.Component} List
- */
- render() {
- console.log('Tracking focus? ' + this.state.trackingFocus);
- return (
- this.node = node}
- className={cn('select', this.props.className)}
- onScroll={this._hideDiff}
- onClick={(e) => e.stopPropagation() }
- onContextMenu={stopCtxPropagation}
- >
- {this.state.list}
-
- );
- }
-
-}
+import React from 'react';
+import PropTypes from 'prop-types';
+import * as ModuleUtils from '../shipyard/ModuleUtils';
+import TranslatedComponent from './TranslatedComponent';
+import { stopCtxPropagation } from '../utils/UtilityFunctions';
+import cn from 'classnames';
+import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
+
+const PRESS_THRESHOLD = 500; // mouse/touch down threshold
+
+/*
+ * Categorisation of module groups
+ */
+const GRPCAT = {
+ 'sg': 'shields',
+ 'bsg': 'shields',
+ 'psg': 'shields',
+ 'scb': 'shields',
+ 'cc': 'limpet controllers',
+ 'fx': 'limpet controllers',
+ 'hb': 'limpet controllers',
+ 'pc': 'limpet controllers',
+ 'rpl': 'limpet controllers',
+ 'pce': 'passenger cabins',
+ 'pci': 'passenger cabins',
+ 'pcm': 'passenger cabins',
+ 'pcq': 'passenger cabins',
+ 'fh': 'hangars',
+ 'pv': 'hangars',
+ 'fs': 'fuel',
+ 'ft': 'fuel',
+ 'hr': 'structural reinforcement',
+ 'mrp': 'structural reinforcement',
+ 'bl': 'lasers',
+ 'pl': 'lasers',
+ 'ul': 'lasers',
+ 'ml': 'lasers',
+ 'c': 'projectiles',
+ 'mc': 'projectiles',
+ 'axmc': 'experimental',
+ 'fc': 'projectiles',
+ 'rfl': 'experimental',
+ 'pa': 'projectiles',
+ 'rg': 'projectiles',
+ 'mr': 'ordnance',
+ 'axmr': 'experimental',
+ 'rcpl': 'experimental',
+ 'tp': 'ordnance',
+ 'nl': 'ordnance',
+ 'sc': 'scanners',
+ 'ss': 'scanners',
+ // Utilities
+ 'cs': 'scanners',
+ 'kw': 'scanners',
+ 'ws': 'scanners',
+ 'xs': 'scanners',
+ 'ch': 'defence',
+ 'po': 'defence',
+ 'ec': 'defence',
+ 'sfn': 'defence',
+ // Standard
+ 'gpp': 'guardian',
+ 'gpc': 'guardian',
+ 'ggc': 'guardian'
+};
+// Order here is the order in which items will be shown in the modules menu
+const CATEGORIES = {
+ // Internals
+ 'am': ['am'],
+ 'cr': ['cr'],
+ 'fi': ['fi'],
+ 'fuel': ['ft', 'fs'],
+ 'hangars': ['fh', 'pv'],
+ 'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'],
+ 'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
+ 'rf': ['rf'],
+ 'shields': ['sg', 'bsg', 'psg', 'scb'],
+ 'structural reinforcement': ['hr', 'mrp'],
+ 'dc': ['dc'],
+ // Hardpoints
+ 'lasers': ['pl', 'ul', 'bl', 'ml'],
+ 'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
+ 'ordnance': ['mr', 'tp', 'nl'],
+ // Utilities
+ 'sb': ['sb'],
+ 'hs': ['hs'],
+ 'defence': ['ch', 'po', 'ec'],
+ 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
+ // Experimental
+ 'experimental': ['axmc', 'axmr', 'rfl', 'xs', 'sfn', 'rcpl'],
+
+ // Guardian
+ 'guardian': ['gpp', 'gpc', 'ggc']
+};
+
+/**
+ * Available modules menu
+ */
+export default class AvailableModulesMenu extends TranslatedComponent {
+
+ static propTypes = {
+ modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
+ onSelect: PropTypes.func.isRequired,
+ diffDetails: PropTypes.func,
+ m: PropTypes.object,
+ shipMass: PropTypes.number,
+ warning: PropTypes.func,
+ firstSlotId: PropTypes.string,
+ lastSlotId: PropTypes.string,
+ activeSlotId: PropTypes.string,
+ slotDiv: PropTypes.object
+ };
+
+ static defaultProps = {
+ shipMass: 0
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ */
+ constructor(props, context) {
+ super(props);
+ this._hideDiff = this._hideDiff.bind(this);
+ this.state = this._initState(props, context);
+ this.slotItems = [];// Array to hold refs.
+ }
+
+ /**
+ * Initiate the list of available moduels
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ * @return {Object} list: Array of React Components, currentGroup Component if any
+ */
+ _initState(props, context) {
+ let translate = context.language.translate;
+ let { m, warning, shipMass, onSelect, modules, firstSlotId, lastSlotId } = props;
+ let list, currentGroup;
+
+ let buildGroup = this._buildGroup.bind(
+ this,
+ translate,
+ m,
+ warning,
+ shipMass - (m && m.mass ? m.mass : 0),
+ (m, event) => {
+ this._hideDiff(event);
+ onSelect(m);
+ }
+ );
+
+ if (modules instanceof Array) {
+ list = buildGroup(modules[0].grp, modules);
+ } else {
+ list = [];
+ // At present time slots with grouped options (Hardpoints and Internal) can be empty
+ if (m) {
+ let emptyId = 'empty';
+ if(this.firstSlotId == null) this.firstSlotId = emptyId;
+ let keyDown = this._keyDown.bind(this, onSelect);
+ list.push( this.slotItems[emptyId] = slotItem} >{translate('empty')}
);
+ }
+
+ // Need to regroup the modules by our own categorisation
+ let catmodules = {};
+ // Pre-create to preserve ordering
+ for (let cat in CATEGORIES) {
+ catmodules[cat] = [];
+ }
+ for (let g in modules) {
+ const moduleCategory = GRPCAT[g] || g;
+ const existing = catmodules[moduleCategory] || [];
+ catmodules[moduleCategory] = existing.concat(modules[g]);
+ }
+
+ for (let category in catmodules) {
+ let categoryHeader = false;
+ // Order through CATEGORIES if present
+ const categories = CATEGORIES[category] || [category];
+ if (categories && categories.length) {
+ for (let n in categories) {
+ const grp = categories[n];
+ // We now have the group and the category. We might not have any modules, though...
+ if (modules[grp]) {
+ // Decide if we need a category header as well as a group header
+ if (categories.length === 1) {
+ // Show category header instead of group header
+ if (m && grp == m.grp) {
+ list.push( this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}
);
+ } else {
+ list.push({translate(category)}
);
+ }
+ } else {
+ // Show category header as well as group header
+ if (!categoryHeader) {
+ list.push({translate(category)}
);
+ categoryHeader = true;
+ }
+ if (m && grp == m.grp) {
+ list.push( this.groupElem = elem} key={grp} className={'select-group cap'}>{translate(grp)}
);
+ } else {
+ list.push({translate(grp)}
);
+ }
+ }
+ list.push(buildGroup(grp, modules[grp]));
+ }
+ }
+ }
+ }
+ }
+ let trackingFocus = false;
+ return { list, currentGroup, trackingFocus };
+ }
+
+ /**
+ * Generate React Components for Module Group
+ * @param {Function} translate Translate function
+ * @param {Object} mountedModule Mounted Module
+ * @param {Function} warningFunc Warning function
+ * @param {number} mass Mass
+ * @param {function} onSelect Select/Mount callback
+ * @param {string} grp Group name
+ * @param {Array} modules Available modules
+ * @return {*} Available Module Group contents
+ * @param {*} firstSlotId ID of the first slot
+ * @param {*} lastSlotId ID of the last slot
+ */
+ _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules, firstSlotId, lastSlotId) {
+ let prevClass = null, prevRating = null, prevName;
+ let elems = [];
+
+ const sortedModules = modules.sort(this._moduleOrder);
+
+
+ // Calculate the number of items per class. Used so we don't have long lists with only a few items in each row
+ const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => { count[cls] = ++count[cls] || 1; return count; }, {});
+ const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
+
+ let itemsOnThisRow = 0;
+ for (let i = 0; i < sortedModules.length; i++) {
+ let m = sortedModules[i];
+ let mount = null;
+ let disabled = false;
+ prevName = m.name;
+ if (ModuleUtils.isShieldGenerator(m.grp)) {
+ // Shield generators care about maximum hull mass
+ disabled = mass > m.maxmass;
+ } else if (m.maxmass) {
+ // Thrusters care about total mass
+ disabled = mass + m.mass > m.maxmass;
+ }
+ let active = mountedModule && mountedModule.id === m.id;
+ let classes = cn(m.name ? 'lc' : 'c', {
+ warning: !disabled && warningFunc && warningFunc(m),
+ active,
+ disabled
+ });
+ let eventHandlers;
+
+ if (disabled) {
+ eventHandlers = {
+ onKeyDown: this._keyDown.bind(this, null),
+ onKeyUp: this._keyUp.bind(this, null)
+
+ };
+ } else {
+ /**
+ * Get the ids of the first and last elements in the that are focusable (i.e. are not active or disabled)
+ * Will be used to keep focus inside the on Tab and Shift-Tab while it is visible
+ */
+ if (this.firstSlotId == null) this.firstSlotId = sortedModules[i].id;
+ if (active) this.activeSlotId = sortedModules[i].id;
+ this.lastSlotId = sortedModules[i].id;
+
+ let showDiff = this._showDiff.bind(this, mountedModule, m);
+ let select = onSelect.bind(null, m);
+
+ eventHandlers = {
+ onMouseEnter: this._over.bind(this, showDiff),
+ onTouchStart: this._touchStart.bind(this, showDiff),
+ onTouchEnd: this._touchEnd.bind(this, select),
+ onMouseLeave: this._hideDiff,
+ onClick: select,
+ onKeyDown: this._keyDown.bind(this, select),
+ onKeyUp: this._keyUp.bind(this, select)
+ };
+ }
+
+ switch(m.mount) {
+ case 'F': mount = ; break;
+ case 'G': mount = ; break;
+ case 'T': mount = ; break;
+ }
+ if (m.name && m.name === prevName) {
+ // elems.push( );
+ itemsOnThisRow = 0;
+ }
+ if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
+ elems.push( );
+ itemsOnThisRow = 0;
+ }
+ let tbIdx = (classes.indexOf('disabled') < 0) ? 0 : undefined;
+ elems.push(
+ this.slotItems[m.id] = slotItem}>
+ {mount}
+ {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
+
+ );
+
+ itemsOnThisRow++;
+ prevClass = m.class;
+ prevRating = m.rating;
+ prevName = m.name;
+ }
+ return ;
+ }
+
+ /**
+ * Generate tooltip content for the difference between the
+ * mounted module and the hovered modules
+ * @param {Object} mm The module mounet currently
+ * @param {Object} m The hovered module
+ * @param {DOMRect} rect DOMRect for target element
+ */
+ _showDiff(mm, m, rect) {
+ if (this.props.diffDetails) {
+ this.touchTimeout = null;
+ this.context.tooltip(this.props.diffDetails(m, mm), rect);
+ }
+ }
+
+ /**
+ * Mouse over diff handler
+ * @param {Function} showDiff diff tooltip callback
+ * @param {SyntheticEvent} event Event
+ */
+ _over(showDiff, event) {
+ event.preventDefault();
+ showDiff(event.currentTarget.getBoundingClientRect());
+ }
+
+ /**
+ * Toucch Start - Show diff after press, otherwise treat as tap
+ * @param {Function} showDiff diff tooltip callback
+ * @param {SyntheticEvent} event Event
+ */
+ _touchStart(showDiff, event) {
+ event.preventDefault();
+ let rect = event.currentTarget.getBoundingClientRect();
+ this.touchTimeout = setTimeout(showDiff.bind(this, rect), PRESS_THRESHOLD);
+ }
+
+ /**
+ * Touch End - Select module on tap
+ * @param {Function} select Select module callback
+ * @param {SyntheticEvent} event Event
+ */
+ _touchEnd(select, event) {
+ event.preventDefault();
+ if (this.touchTimeout !== null) { // If timeout has not fired (been nulled out) yet
+ select();
+ }
+ this._hideDiff();
+ }
+
+ /**
+ * Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
+ * @param {Function} select Select module callback
+ * @param {SyntheticEvent} event Event
+ */
+ _keyDown(select, event) {
+ let className = event.currentTarget.attributes['class'].value;
+ if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
+ select();
+ return;
+ }
+ let elemId = event.currentTarget.attributes['data-id'].value;
+ if (className.indexOf('disabled') < 0 && event.key == 'Tab') {
+ if (event.shiftKey && elemId == this.firstSlotId) {
+ event.preventDefault();
+ this.slotItems[this.lastSlotId].focus();
+ return;
+ }
+ if (!event.shiftKey && elemId == this.lastSlotId) {
+ event.preventDefault();
+ this.slotItems[this.firstSlotId].focus();
+ return;
+ }
+ }
+ }
+
+ /**
+ * Key Up
+ * @param {Function} select Select function
+ * @param {SyntheticEvent} event Event
+ */
+ _keyUp(select,event) {
+ // nothing here yet
+ }
+
+ /**
+ * Hide diff tooltip
+ * @param {SyntheticEvent} event Event
+ */
+ _hideDiff(event) {
+ clearTimeout(this.touchTimeout);
+ this.touchTimeout = null;
+ this.context.tooltip();
+ }
+
+ /**
+ * Order two modules suitably for display in module selection
+ * @param {Object} a the first module
+ * @param {Object} b the second module
+ * @return {int} -1 if the first module should go first, 1 if the second module should go first
+ */
+ _moduleOrder(a, b) {
+ // Named modules go last
+ if (!a.name && b.name) {
+ return -1;
+ }
+ if (a.name && !b.name) {
+ return 1;
+ }
+ // Class ordered from highest (8) to lowest (1)
+ if (a.class < b.class) {
+ return 1;
+ }
+ if (a.class > b.class) {
+ return -1;
+ }
+ // Mount type, if applicable
+ if (a.mount && b.mount && a.mount !== b.mount) {
+ if (a.mount === 'F' || (a.mount === 'G' && b.mount === 'T')) {
+ return -1;
+ } else {
+ return 1;
+ }
+ }
+ // Rating ordered from highest (A) to lowest (E)
+ if (a.rating < b.rating) {
+ return -1;
+ }
+ if (a.rating > b.rating) {
+ return 1;
+ }
+ // Do not attempt to order by name at this point, as that mucks up the order of armour
+ return 0;
+ }
+
+ /**
+ * Scroll to mounted (if it exists) module group on mount
+ */
+ componentDidMount() {
+ if (this.groupElem) { // Scroll to currently selected group
+ this.node.scrollTop = this.groupElem.offsetTop;
+ }
+ /**
+ * Set focus on active or first slot element, if applicable.
+ */
+ if (this.slotItems[this.activeSlotId]) {
+ this.slotItems[this.activeSlotId].focus();
+ } else if (this.slotItems[this.firstSlotId]) {
+ this.slotItems[this.firstSlotId].focus();
+ }
+ }
+ /**
+ * Set focus to slot element ref (if we have one) after modules component unmounts
+ */
+ componentWillUnmount() {
+ if(this.props.slotDiv) {
+ this.props.slotDiv.focus();
+ }
+ }
+
+ /**
+ * Update state based on property and context changes
+ * @param {Object} nextProps Incoming/Next properties
+ * @param {Object} nextContext Incoming/Next conext
+ */
+ componentWillReceiveProps(nextProps, nextContext) {
+ this.setState(this._initState(nextProps, nextContext));
+ }
+
+ /**
+ * Render the list
+ * @return {React.Component} List
+ */
+ render() {
+ console.log('Tracking focus? ' + this.state.trackingFocus);
+ return (
+ this.node = node}
+ className={cn('select', this.props.className)}
+ onScroll={this._hideDiff}
+ onClick={(e) => e.stopPropagation() }
+ onContextMenu={stopCtxPropagation}
+ >
+ {this.state.list}
+
+ );
+ }
+
+}
diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx
index 56fd7a88..8755e836 100644
--- a/src/app/components/ModificationsMenu.jsx
+++ b/src/app/components/ModificationsMenu.jsx
@@ -1,476 +1,476 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import * as _ from 'lodash';
-import TranslatedComponent from './TranslatedComponent';
-import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions';
-import cn from 'classnames';
-import { Modifications } from 'coriolis-data/dist';
-import Modification from './Modification';
-import {
- getBlueprint,
- blueprintTooltip,
- setPercent,
- getPercent,
- setRandom,
- specialToolTip
-} from '../utils/BlueprintFunctions';
-
-/**
- * Modifications menu
- */
-export default class ModificationsMenu extends TranslatedComponent {
-
- static propTypes = {
- ship: PropTypes.object.isRequired,
- m: PropTypes.object.isRequired,
- marker: PropTypes.string.isRequired,
- onChange: PropTypes.func.isRequired,
- modButton:PropTypes.object
- };
-
- /**
- * Constructor
- * @param {Object} props React Component properties
- * @param {Object} context React Component context
- */
- constructor(props, context) {
- super(props);
-
- this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
- this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
- this._rollFifty = this._rollFifty.bind(this);
- this._rollRandom = this._rollRandom.bind(this);
- this._rollBest = this._rollBest.bind(this);
- this._rollWorst = this._rollWorst.bind(this);
- this._reset = this._reset.bind(this);
-
- this._keyDown = this._keyDown.bind(this);
-
- this.modItems = [];// Array to hold various element refs (, ,
, etc.)
- this.firstModId = null;
- this.firstBPLabel = null;// First item in mod menu
- this.lastModId = null;
- this.lastNeId = null;// Last number editor id. Used to set focus to last number editor when shift-tab pressed on first element in mod menu.
- this.modValDidChange = false; // used to determine if component update was caused by change in modification value.
- this._handleModChange = this._handleModChange.bind(this);
-
- this.state = {
- blueprintMenuOpened: !(props.m.blueprint && props.m.blueprint.name),
- specialMenuOpened: false
- };
- }
-
- /**
- * Render the blueprints
- * @param {Object} props React component properties
- * @param {Object} context React component context
- * @return {Object} list: Array of React Components
- */
- _renderBlueprints(props, context) {
- const { m } = props;
- const { language, tooltip, termtip } = context;
- const translate = language.translate;
- const blueprints = [];
- for (const blueprintName in Modifications.modules[m.grp].blueprints) {
- const blueprint = getBlueprint(blueprintName, m);
- let blueprintGrades = [];
- for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
- // Grade is a string in the JSON so make it a number
- grade = Number(grade);
- const classes = cn('c', {
- active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade
- });
- const close = this._blueprintSelected.bind(this, blueprintName, grade);
- const key = blueprintName + ':' + grade;
- const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
-
-
- blueprintGrades.unshift( this.modItems[key] = modItem}>{grade} );
- }
- if (blueprintGrades) {
- const thisLen = blueprintGrades.length;
- if (this.firstModId == null) this.firstModId = blueprintGrades[0].key;
- this.lastModId = blueprintGrades[thisLen - 1].key;
- blueprints.push({translate(blueprint.name)}
);
- blueprints.push();
- }
- }
- return blueprints;
- }
-
- /**
- * Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
- * @param {Function} event Select module callback
- */
- _keyDown(event) {
- let className = null;
- let elemId = null;
- if (event.currentTarget.attributes['class']) className = event.currentTarget.attributes['class'].value;
- if (event.currentTarget.attributes['data-id']) elemId = event.currentTarget.attributes['data-id'].value;
-
- if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
- event.stopPropagation();
- if (elemId != null) {
- this.modItems[elemId].click();
- } else {
- event.currentTarget.click();
- }
- return;
- }
- if (event.key == 'Tab') {
- // Shift-Tab
- if(event.shiftKey) {
- if (elemId == this.firstModId && elemId != null) {
- // Initial modification menu
- event.preventDefault();
- this.modItems[this.lastModId].focus();
- return;
- } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null && this.lastNeId != null && this.modItems[this.lastNeId] != null) {
- // shift-tab on first element in modifications menu. set focus to last number editor field if open
- event.preventDefault();
- this.modItems[this.lastNeId].lastChild.focus();
- return;
- } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null) {
- // shift-tab on button-inline-menu with no number editor
- event.preventDefault();
- event.currentTarget.parentElement.lastElementChild.focus();
- }
- } else {
- if (elemId == this.lastModId && elemId != null) {
- // Initial modification menu
- event.preventDefault();
- this.modItems[this.firstModId].focus();
- return;
- } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.nextSibling == null && event.currentTarget.nodeName != 'TD') {
- // Experimental menu
- event.preventDefault();
- event.currentTarget.parentElement.firstElementChild.focus();
- return;
- } else if (event.currentTarget.className == 'cb' && event.currentTarget.parentElement.nextSibling == null) {
- event.preventDefault();
- this.modItems[this.firstBPLabel].focus();
- }
- }
- }
- }
-
-
- /**
- * Render the specials
- * @param {Object} props React component properties
- * @param {Object} context React component context
- * @return {Object} list: Array of React Components
- */
- _renderSpecials(props, context) {
- const { m } = props;
- const { language, tooltip, termtip } = context;
- const translate = language.translate;
- const specials = [];
- const specialsId = m.missile && Modifications.modules[m.grp]['specials_' + m.missile] ? 'specials_' + m.missile : 'specials';
- if (Modifications.modules[m.grp][specialsId] && Modifications.modules[m.grp][specialsId].length > 0) {
- const close = this._specialSelected.bind(this, null);
- specials.push( this.modItems['none'] = modItem}>{translate('PHRASE_NO_SPECIAL')}
);
- for (const specialName of Modifications.modules[m.grp][specialsId]) {
- if (Modifications.specials[specialName].name.search('Legacy') >= 0) {
- continue;
- }
- const classes = cn('button-inline-menu', {
- active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
- });
- const close = this._specialSelected.bind(this, specialName);
- if (m.blueprint && m.blueprint.name) {
- let tmp = {};
- if (m.blueprint.special) {
- tmp = m.blueprint.special;
- } else {
- tmp = undefined;
- }
- m.blueprint.special = Modifications.specials[specialName];
- let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
- m.blueprint.special = tmp;
- specials.push( this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}
);
- } else {
- specials.push( this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}
);
- }
- }
- }
- console.log('_renderSpecials. specials: %O', specials);
- return specials;
- }
-
- /**
- * Render the modifications
- * @param {Object} props React Component properties
- * @return {Object} list: Array of React Components
- */
- _renderModifications(props) {
- const { m, onChange, ship } = props;
- const modifications = [];
- for (const modName of Modifications.modules[m.grp].modifications) {
- if (!Modifications.modifications[modName].hidden) {
- const key = modName + (m.getModValue(modName) / 100 || 0);
- this.lastNeId = modName;
- modifications.push( );
- }
- }
- console.log('_renderModifications. modItems: %O', this.modItems);
- return modifications;
- }
-
- /**
- * Toggle the blueprints menu
- */
- _toggleBlueprintsMenu() {
- const blueprintMenuOpened = !this.state.blueprintMenuOpened;
- this.setState({ blueprintMenuOpened });
- }
-
- /**
- * Activated when a blueprint is selected
- * @param {int} fdname The Frontier name of the blueprint
- * @param {int} grade The grade of the selected blueprint
- */
- _blueprintSelected(fdname, grade) {
- this.context.tooltip(null);
- const { m, ship } = this.props;
- const blueprint = getBlueprint(fdname, m);
- blueprint.grade = grade;
- ship.setModuleBlueprint(m, blueprint);
- setPercent(ship, m, 100);
-
- this.setState({ blueprintMenuOpened: false, specialMenuOpened: true });
- this.props.onChange();
- }
-
- /**
- * Toggle the specials menu
- */
- _toggleSpecialsMenu() {
- const specialMenuOpened = !this.state.specialMenuOpened;
- this.setState({ specialMenuOpened });
- }
-
- /**
- * Activated when a special is selected
- * @param {int} special The name of the selected special
- */
- _specialSelected(special) {
- this.context.tooltip(null);
- const { m, ship } = this.props;
-
- if (special === null) {
- ship.clearModuleSpecial(m);
- } else {
- ship.setModuleSpecial(m, Modifications.specials[special]);
- }
-
- this.setState({ specialMenuOpened: false });
- this.props.onChange();
- }
-
- /**
- * Provide a '50%' roll within the information we have
- */
- _rollFifty() {
- const { m, ship } = this.props;
- setPercent(ship, m, 50);
-
- // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
- this._handleModChange(true);
-
- this.props.onChange();
- }
-
- /**
- * Provide a random roll within the information we have
- */
- _rollRandom() {
- const { m, ship } = this.props;
- setRandom(ship, m);
-
- // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
- this._handleModChange(true);
-
- this.props.onChange();
- }
-
- /**
- * Provide a 'best' roll within the information we have
- */
- _rollBest() {
- const { m, ship } = this.props;
- setPercent(ship, m, 100);
-
- // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
- this._handleModChange(true);
-
- this.props.onChange();
- }
-
- /**
- * Provide a 'worst' roll within the information we have
- */
- _rollWorst() {
- const { m, ship } = this.props;
- setPercent(ship, m, 0);
-
- // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
- this._handleModChange(true);
-
- this.props.onChange();
- }
-
- /**
- * Reset modification information
- */
- _reset() {
- const { m, ship } = this.props;
- ship.clearModifications(m);
- ship.clearModuleBlueprint(m);
- this.props.onChange();
- }
-
- /**
- * set mod did change boolean
- * @param {Boolean} b Did the mod value change?
- */
- _handleModChange(b) {
- this.modValDidChange = b;
- }
- /**
- * Set focus on first element in modifications menu
- * after it first mounts
- */
- componentDidMount() {
- let firstEleCn = this.modItems['modMainDiv'].children.length > 0 ? this.modItems['modMainDiv'].children[0].className : null;
- if (firstEleCn.indexOf('select-group cap') >= 0) {
- this.modItems['modMainDiv'].children[1].firstElementChild.focus();
- } else {
- this.modItems['modMainDiv'].firstElementChild.focus();
- }
- }
- /**
- * Set focus on first element in modifications menu
- * if component updates, unless update is due to value change
- * in a modification
- */
- componentDidUpdate() {
- if (!this.modValDidChange) {
- if (this.modItems['modMainDiv'].children.length > 0) {
- let firstEleCn = this.modItems['modMainDiv'].children[0].className;
- if (firstEleCn.indexOf('button-inline-menu') >= 0) {
- this.modItems['modMainDiv'].firstElementChild.focus();
- } else if (firstEleCn.indexOf('select-group cap') >= 0) {
- this.modItems['modMainDiv'].children[1].firstElementChild.focus();
- }
- }
- } else {
- this._handleModChange(false);// Need to reset if component update due to value change
- }
- }
-
- /**
- * Set focus to the modification menu icon
- */
- componentWillUnmount() {
- if (this.props.modButton) {
- this.props.modButton.focus();// set focus to the modification menu icon after mod menu is unmounted.
- }
- }
-
- /**
- * Render the list
- * @return {React.Component} List
- */
- render() {
- const { language, tooltip, termtip } = this.context;
- const translate = language.translate;
- const { m } = this.props;
- const { blueprintMenuOpened, specialMenuOpened } = this.state;
-
- const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
- const _toggleSpecialsMenu = this._toggleSpecialsMenu;
- const _rollFull = this._rollBest;
- const _rollWorst = this._rollWorst;
- const _rollFifty = this._rollFifty;
- const _rollRandom = this._rollRandom;
- const _reset = this._reset;
-
- let blueprintLabel;
- let haveBlueprint = false;
- let blueprintTt;
- let blueprintCv;
- // TODO: Fix this to actually find the correct blueprint.
- if (!m.blueprint || !m.blueprint.name || !m.blueprint.fdname || !Modifications.modules[m.grp].blueprints || !Modifications.modules[m.grp].blueprints[m.blueprint.fdname]) {
- this.props.ship.clearModuleBlueprint(m);
- this.props.ship.clearModuleSpecial(m);
- }
- if (m.blueprint && m.blueprint.name && Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade]) {
- blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
- haveBlueprint = true;
- blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
- blueprintCv = getPercent(m);
- }
-
- let specialLabel;
- let specialTt;
- if (m.blueprint && m.blueprint.special) {
- specialLabel = m.blueprint.special.name;
- specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
- } else {
- specialLabel = translate('PHRASE_SELECT_SPECIAL');
- }
-
- const specials = this._renderSpecials(this.props, this.context);
- /**
- * pnellesen - 05/28/2018 - added additional checks for specials.length below to ensure menus
- * display correctly in cases where there are no specials (ex: AFMUs.)
- */
- const showBlueprintsMenu = blueprintMenuOpened;
- const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
- const showSpecialsMenu = specialMenuOpened && specials.length;
- const showRolls = haveBlueprint && !blueprintMenuOpened && (!specialMenuOpened || !specials.length);
- const showReset = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
- const showMods = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
- if (haveBlueprint) {
- this.firstBPLabel = blueprintLabel;
- } else {
- this.firstBPLabel = 'selectBP';
- }
- return (
- e.stopPropagation() }
- onContextMenu={stopCtxPropagation}
- ref={modItem => this.modItems['modMainDiv'] = modItem}
- >
- { showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
-
this.modItems[this.firstBPLabel] = modItems}>{blueprintLabel}
:
-
this.modItems[this.firstBPLabel] = modItems}>{translate('PHRASE_SELECT_BLUEPRINT')}
}
- { showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
- { showSpecial & !showSpecialsMenu ?
{specialLabel}
: null }
- { showSpecialsMenu ? specials : null }
- { showReset ?
{ translate('reset') }
: null }
- { showRolls ?
-
-
-
- { showRolls ?
-
- { translate('roll') }:
- { translate('0%') }
- { translate('50%') }
- { translate('100%') }
- { translate('random') }
- : null }
-
-
: null }
- { showMods ?
: null }
- { showMods ?
-
- { this._renderModifications(this.props) }
- : null }
-
- );
- }
-}
+import React from 'react';
+import PropTypes from 'prop-types';
+import * as _ from 'lodash';
+import TranslatedComponent from './TranslatedComponent';
+import { isEmpty, stopCtxPropagation } from '../utils/UtilityFunctions';
+import cn from 'classnames';
+import { Modifications } from 'coriolis-data/dist';
+import Modification from './Modification';
+import {
+ getBlueprint,
+ blueprintTooltip,
+ setPercent,
+ getPercent,
+ setRandom,
+ specialToolTip
+} from '../utils/BlueprintFunctions';
+
+/**
+ * Modifications menu
+ */
+export default class ModificationsMenu extends TranslatedComponent {
+
+ static propTypes = {
+ ship: PropTypes.object.isRequired,
+ m: PropTypes.object.isRequired,
+ marker: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ modButton:PropTypes.object
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ */
+ constructor(props, context) {
+ super(props);
+
+ this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
+ this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
+ this._rollFifty = this._rollFifty.bind(this);
+ this._rollRandom = this._rollRandom.bind(this);
+ this._rollBest = this._rollBest.bind(this);
+ this._rollWorst = this._rollWorst.bind(this);
+ this._reset = this._reset.bind(this);
+
+ this._keyDown = this._keyDown.bind(this);
+
+ this.modItems = [];// Array to hold various element refs (, ,
, etc.)
+ this.firstModId = null;
+ this.firstBPLabel = null;// First item in mod menu
+ this.lastModId = null;
+ this.lastNeId = null;// Last number editor id. Used to set focus to last number editor when shift-tab pressed on first element in mod menu.
+ this.modValDidChange = false; // used to determine if component update was caused by change in modification value.
+ this._handleModChange = this._handleModChange.bind(this);
+
+ this.state = {
+ blueprintMenuOpened: !(props.m.blueprint && props.m.blueprint.name),
+ specialMenuOpened: false
+ };
+ }
+
+ /**
+ * Render the blueprints
+ * @param {Object} props React component properties
+ * @param {Object} context React component context
+ * @return {Object} list: Array of React Components
+ */
+ _renderBlueprints(props, context) {
+ const { m } = props;
+ const { language, tooltip, termtip } = context;
+ const translate = language.translate;
+ const blueprints = [];
+ for (const blueprintName in Modifications.modules[m.grp].blueprints) {
+ const blueprint = getBlueprint(blueprintName, m);
+ let blueprintGrades = [];
+ for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
+ // Grade is a string in the JSON so make it a number
+ grade = Number(grade);
+ const classes = cn('c', {
+ active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade
+ });
+ const close = this._blueprintSelected.bind(this, blueprintName, grade);
+ const key = blueprintName + ':' + grade;
+ const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
+
+
+ blueprintGrades.unshift( this.modItems[key] = modItem}>{grade} );
+ }
+ if (blueprintGrades) {
+ const thisLen = blueprintGrades.length;
+ if (this.firstModId == null) this.firstModId = blueprintGrades[0].key;
+ this.lastModId = blueprintGrades[thisLen - 1].key;
+ blueprints.push({translate(blueprint.name)}
);
+ blueprints.push();
+ }
+ }
+ return blueprints;
+ }
+
+ /**
+ * Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
+ * @param {Function} event Select module callback
+ */
+ _keyDown(event) {
+ let className = null;
+ let elemId = null;
+ if (event.currentTarget.attributes['class']) className = event.currentTarget.attributes['class'].value;
+ if (event.currentTarget.attributes['data-id']) elemId = event.currentTarget.attributes['data-id'].value;
+
+ if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
+ event.stopPropagation();
+ if (elemId != null) {
+ this.modItems[elemId].click();
+ } else {
+ event.currentTarget.click();
+ }
+ return;
+ }
+ if (event.key == 'Tab') {
+ // Shift-Tab
+ if(event.shiftKey) {
+ if (elemId == this.firstModId && elemId != null) {
+ // Initial modification menu
+ event.preventDefault();
+ this.modItems[this.lastModId].focus();
+ return;
+ } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null && this.lastNeId != null && this.modItems[this.lastNeId] != null) {
+ // shift-tab on first element in modifications menu. set focus to last number editor field if open
+ event.preventDefault();
+ this.modItems[this.lastNeId].lastChild.focus();
+ return;
+ } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null) {
+ // shift-tab on button-inline-menu with no number editor
+ event.preventDefault();
+ event.currentTarget.parentElement.lastElementChild.focus();
+ }
+ } else {
+ if (elemId == this.lastModId && elemId != null) {
+ // Initial modification menu
+ event.preventDefault();
+ this.modItems[this.firstModId].focus();
+ return;
+ } else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.nextSibling == null && event.currentTarget.nodeName != 'TD') {
+ // Experimental menu
+ event.preventDefault();
+ event.currentTarget.parentElement.firstElementChild.focus();
+ return;
+ } else if (event.currentTarget.className == 'cb' && event.currentTarget.parentElement.nextSibling == null) {
+ event.preventDefault();
+ this.modItems[this.firstBPLabel].focus();
+ }
+ }
+ }
+ }
+
+
+ /**
+ * Render the specials
+ * @param {Object} props React component properties
+ * @param {Object} context React component context
+ * @return {Object} list: Array of React Components
+ */
+ _renderSpecials(props, context) {
+ const { m } = props;
+ const { language, tooltip, termtip } = context;
+ const translate = language.translate;
+ const specials = [];
+ const specialsId = m.missile && Modifications.modules[m.grp]['specials_' + m.missile] ? 'specials_' + m.missile : 'specials';
+ if (Modifications.modules[m.grp][specialsId] && Modifications.modules[m.grp][specialsId].length > 0) {
+ const close = this._specialSelected.bind(this, null);
+ specials.push( this.modItems['none'] = modItem}>{translate('PHRASE_NO_SPECIAL')}
);
+ for (const specialName of Modifications.modules[m.grp][specialsId]) {
+ if (Modifications.specials[specialName].name.search('Legacy') >= 0) {
+ continue;
+ }
+ const classes = cn('button-inline-menu', {
+ active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
+ });
+ const close = this._specialSelected.bind(this, specialName);
+ if (m.blueprint && m.blueprint.name) {
+ let tmp = {};
+ if (m.blueprint.special) {
+ tmp = m.blueprint.special;
+ } else {
+ tmp = undefined;
+ }
+ m.blueprint.special = Modifications.specials[specialName];
+ let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
+ m.blueprint.special = tmp;
+ specials.push( this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}
);
+ } else {
+ specials.push( this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}
);
+ }
+ }
+ }
+ console.log('_renderSpecials. specials: %O', specials);
+ return specials;
+ }
+
+ /**
+ * Render the modifications
+ * @param {Object} props React Component properties
+ * @return {Object} list: Array of React Components
+ */
+ _renderModifications(props) {
+ const { m, onChange, ship } = props;
+ const modifications = [];
+ for (const modName of Modifications.modules[m.grp].modifications) {
+ if (!Modifications.modifications[modName].hidden) {
+ const key = modName + (m.getModValue(modName) / 100 || 0);
+ this.lastNeId = modName;
+ modifications.push( );
+ }
+ }
+ console.log('_renderModifications. modItems: %O', this.modItems);
+ return modifications;
+ }
+
+ /**
+ * Toggle the blueprints menu
+ */
+ _toggleBlueprintsMenu() {
+ const blueprintMenuOpened = !this.state.blueprintMenuOpened;
+ this.setState({ blueprintMenuOpened });
+ }
+
+ /**
+ * Activated when a blueprint is selected
+ * @param {int} fdname The Frontier name of the blueprint
+ * @param {int} grade The grade of the selected blueprint
+ */
+ _blueprintSelected(fdname, grade) {
+ this.context.tooltip(null);
+ const { m, ship } = this.props;
+ const blueprint = getBlueprint(fdname, m);
+ blueprint.grade = grade;
+ ship.setModuleBlueprint(m, blueprint);
+ setPercent(ship, m, 100);
+
+ this.setState({ blueprintMenuOpened: false, specialMenuOpened: true });
+ this.props.onChange();
+ }
+
+ /**
+ * Toggle the specials menu
+ */
+ _toggleSpecialsMenu() {
+ const specialMenuOpened = !this.state.specialMenuOpened;
+ this.setState({ specialMenuOpened });
+ }
+
+ /**
+ * Activated when a special is selected
+ * @param {int} special The name of the selected special
+ */
+ _specialSelected(special) {
+ this.context.tooltip(null);
+ const { m, ship } = this.props;
+
+ if (special === null) {
+ ship.clearModuleSpecial(m);
+ } else {
+ ship.setModuleSpecial(m, Modifications.specials[special]);
+ }
+
+ this.setState({ specialMenuOpened: false });
+ this.props.onChange();
+ }
+
+ /**
+ * Provide a '50%' roll within the information we have
+ */
+ _rollFifty() {
+ const { m, ship } = this.props;
+ setPercent(ship, m, 50);
+
+ // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
+ this._handleModChange(true);
+
+ this.props.onChange();
+ }
+
+ /**
+ * Provide a random roll within the information we have
+ */
+ _rollRandom() {
+ const { m, ship } = this.props;
+ setRandom(ship, m);
+
+ // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
+ this._handleModChange(true);
+
+ this.props.onChange();
+ }
+
+ /**
+ * Provide a 'best' roll within the information we have
+ */
+ _rollBest() {
+ const { m, ship } = this.props;
+ setPercent(ship, m, 100);
+
+ // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
+ this._handleModChange(true);
+
+ this.props.onChange();
+ }
+
+ /**
+ * Provide a 'worst' roll within the information we have
+ */
+ _rollWorst() {
+ const { m, ship } = this.props;
+ setPercent(ship, m, 0);
+
+ // this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
+ this._handleModChange(true);
+
+ this.props.onChange();
+ }
+
+ /**
+ * Reset modification information
+ */
+ _reset() {
+ const { m, ship } = this.props;
+ ship.clearModifications(m);
+ ship.clearModuleBlueprint(m);
+ this.props.onChange();
+ }
+
+ /**
+ * set mod did change boolean
+ * @param {Boolean} b Did the mod value change?
+ */
+ _handleModChange(b) {
+ this.modValDidChange = b;
+ }
+ /**
+ * Set focus on first element in modifications menu
+ * after it first mounts
+ */
+ componentDidMount() {
+ let firstEleCn = this.modItems['modMainDiv'].children.length > 0 ? this.modItems['modMainDiv'].children[0].className : null;
+ if (firstEleCn.indexOf('select-group cap') >= 0) {
+ this.modItems['modMainDiv'].children[1].firstElementChild.focus();
+ } else {
+ this.modItems['modMainDiv'].firstElementChild.focus();
+ }
+ }
+ /**
+ * Set focus on first element in modifications menu
+ * if component updates, unless update is due to value change
+ * in a modification
+ */
+ componentDidUpdate() {
+ if (!this.modValDidChange) {
+ if (this.modItems['modMainDiv'].children.length > 0) {
+ let firstEleCn = this.modItems['modMainDiv'].children[0].className;
+ if (firstEleCn.indexOf('button-inline-menu') >= 0) {
+ this.modItems['modMainDiv'].firstElementChild.focus();
+ } else if (firstEleCn.indexOf('select-group cap') >= 0) {
+ this.modItems['modMainDiv'].children[1].firstElementChild.focus();
+ }
+ }
+ } else {
+ this._handleModChange(false);// Need to reset if component update due to value change
+ }
+ }
+
+ /**
+ * Set focus to the modification menu icon
+ */
+ componentWillUnmount() {
+ if (this.props.modButton) {
+ this.props.modButton.focus();// set focus to the modification menu icon after mod menu is unmounted.
+ }
+ }
+
+ /**
+ * Render the list
+ * @return {React.Component} List
+ */
+ render() {
+ const { language, tooltip, termtip } = this.context;
+ const translate = language.translate;
+ const { m } = this.props;
+ const { blueprintMenuOpened, specialMenuOpened } = this.state;
+
+ const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
+ const _toggleSpecialsMenu = this._toggleSpecialsMenu;
+ const _rollFull = this._rollBest;
+ const _rollWorst = this._rollWorst;
+ const _rollFifty = this._rollFifty;
+ const _rollRandom = this._rollRandom;
+ const _reset = this._reset;
+
+ let blueprintLabel;
+ let haveBlueprint = false;
+ let blueprintTt;
+ let blueprintCv;
+ // TODO: Fix this to actually find the correct blueprint.
+ if (!m.blueprint || !m.blueprint.name || !m.blueprint.fdname || !Modifications.modules[m.grp].blueprints || !Modifications.modules[m.grp].blueprints[m.blueprint.fdname]) {
+ this.props.ship.clearModuleBlueprint(m);
+ this.props.ship.clearModuleSpecial(m);
+ }
+ if (m.blueprint && m.blueprint.name && Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade]) {
+ blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
+ haveBlueprint = true;
+ blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
+ blueprintCv = getPercent(m);
+ }
+
+ let specialLabel;
+ let specialTt;
+ if (m.blueprint && m.blueprint.special) {
+ specialLabel = m.blueprint.special.name;
+ specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
+ } else {
+ specialLabel = translate('PHRASE_SELECT_SPECIAL');
+ }
+
+ const specials = this._renderSpecials(this.props, this.context);
+ /**
+ * pnellesen - 05/28/2018 - added additional checks for specials.length below to ensure menus
+ * display correctly in cases where there are no specials (ex: AFMUs.)
+ */
+ const showBlueprintsMenu = blueprintMenuOpened;
+ const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
+ const showSpecialsMenu = specialMenuOpened && specials.length;
+ const showRolls = haveBlueprint && !blueprintMenuOpened && (!specialMenuOpened || !specials.length);
+ const showReset = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
+ const showMods = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
+ if (haveBlueprint) {
+ this.firstBPLabel = blueprintLabel;
+ } else {
+ this.firstBPLabel = 'selectBP';
+ }
+ return (
+ e.stopPropagation() }
+ onContextMenu={stopCtxPropagation}
+ ref={modItem => this.modItems['modMainDiv'] = modItem}
+ >
+ { showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
+
this.modItems[this.firstBPLabel] = modItems}>{blueprintLabel}
:
+
this.modItems[this.firstBPLabel] = modItems}>{translate('PHRASE_SELECT_BLUEPRINT')}
}
+ { showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
+ { showSpecial & !showSpecialsMenu ?
{specialLabel}
: null }
+ { showSpecialsMenu ? specials : null }
+ { showReset ?
{ translate('reset') }
: null }
+ { showRolls ?
+
+
+
+ { showRolls ?
+
+ { translate('roll') }:
+ { translate('0%') }
+ { translate('50%') }
+ { translate('100%') }
+ { translate('random') }
+ : null }
+
+
: null }
+ { showMods ?
: null }
+ { showMods ?
+
+ { this._renderModifications(this.props) }
+ : null }
+
+ );
+ }
+}
diff --git a/src/app/components/Slot.jsx b/src/app/components/Slot.jsx
index 206a06d3..e76b3795 100644
--- a/src/app/components/Slot.jsx
+++ b/src/app/components/Slot.jsx
@@ -1,164 +1,164 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import TranslatedComponent from './TranslatedComponent';
-import cn from 'classnames';
-import AvailableModulesMenu from './AvailableModulesMenu';
-import ModificationsMenu from './ModificationsMenu';
-import { diffDetails } from '../utils/SlotFunctions';
-import { wrapCtxMenu } from '../utils/UtilityFunctions';
-import { stopCtxPropagation } from '../utils/UtilityFunctions';
-
-/**
- * Abstract Slot
- */
-export default class Slot extends TranslatedComponent {
-
- static propTypes = {
- availableModules: PropTypes.func.isRequired,
- onSelect: PropTypes.func.isRequired,
- onOpen: PropTypes.func.isRequired,
- maxClass: PropTypes.number.isRequired,
- selected: PropTypes.bool,
- m: PropTypes.object,
- enabled: PropTypes.bool.isRequired,
- ship: PropTypes.object.isRequired,
- eligible: PropTypes.object,
- warning: PropTypes.func,
- drag: PropTypes.func,
- drop: PropTypes.func,
- dropClass: PropTypes.string
- };
-
- /**
- * Constructor
- * @param {Object} props React Component properties
- */
- constructor(props) {
- super(props);
-
- this._modificationsSelected = false;
-
- this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
- this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
- this._keyDown = this._keyDown.bind(this);
- this.slotDiv = null;
- }
-
- // Must be implemented by subclasses:
- // _getSlotDetails()
-
- /**
- * Get the CSS class name for the slot. Can/should be overriden
- * as necessary.
- * @return {string} CSS Class name
- */
- _getClassNames() {
- return null;
- }
-
- /**
- * Get the label for the slot size/class
- * Should be overriden if necessary
- * @return {string} label
- */
- _getMaxClassLabel() {
- return this.props.maxClass;
- }
-
- /**
- * Empty slot on right-click
- * @param {SyntheticEvent} event Event
- */
- _contextMenu(event) {
- event.stopPropagation();
- event.preventDefault();
- this.props.onSelect(null,null);
- }
-
- /** Key Down handler
- * @param {SyntheticEvent} event Event
- * ToDo: see if this can be moved up
- * we do more or less the same thing
- * in every section when Enter key is pressed
- * on a focusable item
- *
- */
- _keyDown(event) {
- if (event.key == 'Enter') {
- if(event.target.className == 'r') {
- console.log('Slot: Enter key pressed on mod icon');
- this._toggleModifications();
- } else {
- console.log('Slot: Enter key pressed on: %O', event.target);
- }
- this.props.onOpen(event);
- }
- }
- /**
- * Render the slot
- * @return {React.Component} The slot
- */
- render() {
- let language = this.context.language;
- let translate = language.translate;
- let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
- let slotDetails, modificationsMarker, menu;
-
- if (!selected) {
- // If not selected then sure that modifications flag is unset
- this._modificationsSelected = false;
- }
-
- if (m) {
- slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes
- modificationsMarker = JSON.stringify(m);
- } else {
- slotDetails = {translate(eligible ? 'emptyrestricted' : 'empty')}
;
- modificationsMarker = '';
- }
-
- if (selected) {
- if (this._modificationsSelected) {
- menu = ;
- } else {
- menu = ;
- }
- }
-
- // TODO: implement touch dragging
-
- return (
- this.slotDiv = slotDiv}>
-
-
{this._getMaxClassLabel(translate)}
- {slotDetails}
-
- {menu}
-
- );
- }
-
-
- /**
- * Toggle the modifications flag when selecting the modifications icon
- */
- _toggleModifications() {
- this._modificationsSelected = !this._modificationsSelected;
- }
-}
+import React from 'react';
+import PropTypes from 'prop-types';
+import TranslatedComponent from './TranslatedComponent';
+import cn from 'classnames';
+import AvailableModulesMenu from './AvailableModulesMenu';
+import ModificationsMenu from './ModificationsMenu';
+import { diffDetails } from '../utils/SlotFunctions';
+import { wrapCtxMenu } from '../utils/UtilityFunctions';
+import { stopCtxPropagation } from '../utils/UtilityFunctions';
+
+/**
+ * Abstract Slot
+ */
+export default class Slot extends TranslatedComponent {
+
+ static propTypes = {
+ availableModules: PropTypes.func.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onOpen: PropTypes.func.isRequired,
+ maxClass: PropTypes.number.isRequired,
+ selected: PropTypes.bool,
+ m: PropTypes.object,
+ enabled: PropTypes.bool.isRequired,
+ ship: PropTypes.object.isRequired,
+ eligible: PropTypes.object,
+ warning: PropTypes.func,
+ drag: PropTypes.func,
+ drop: PropTypes.func,
+ dropClass: PropTypes.string
+ };
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ */
+ constructor(props) {
+ super(props);
+
+ this._modificationsSelected = false;
+
+ this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
+ this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
+ this._keyDown = this._keyDown.bind(this);
+ this.slotDiv = null;
+ }
+
+ // Must be implemented by subclasses:
+ // _getSlotDetails()
+
+ /**
+ * Get the CSS class name for the slot. Can/should be overriden
+ * as necessary.
+ * @return {string} CSS Class name
+ */
+ _getClassNames() {
+ return null;
+ }
+
+ /**
+ * Get the label for the slot size/class
+ * Should be overriden if necessary
+ * @return {string} label
+ */
+ _getMaxClassLabel() {
+ return this.props.maxClass;
+ }
+
+ /**
+ * Empty slot on right-click
+ * @param {SyntheticEvent} event Event
+ */
+ _contextMenu(event) {
+ event.stopPropagation();
+ event.preventDefault();
+ this.props.onSelect(null,null);
+ }
+
+ /** Key Down handler
+ * @param {SyntheticEvent} event Event
+ * ToDo: see if this can be moved up
+ * we do more or less the same thing
+ * in every section when Enter key is pressed
+ * on a focusable item
+ *
+ */
+ _keyDown(event) {
+ if (event.key == 'Enter') {
+ if(event.target.className == 'r') {
+ console.log('Slot: Enter key pressed on mod icon');
+ this._toggleModifications();
+ } else {
+ console.log('Slot: Enter key pressed on: %O', event.target);
+ }
+ this.props.onOpen(event);
+ }
+ }
+ /**
+ * Render the slot
+ * @return {React.Component} The slot
+ */
+ render() {
+ let language = this.context.language;
+ let translate = language.translate;
+ let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
+ let slotDetails, modificationsMarker, menu;
+
+ if (!selected) {
+ // If not selected then sure that modifications flag is unset
+ this._modificationsSelected = false;
+ }
+
+ if (m) {
+ slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes
+ modificationsMarker = JSON.stringify(m);
+ } else {
+ slotDetails = {translate(eligible ? 'emptyrestricted' : 'empty')}
;
+ modificationsMarker = '';
+ }
+
+ if (selected) {
+ if (this._modificationsSelected) {
+ menu = ;
+ } else {
+ menu = ;
+ }
+ }
+
+ // TODO: implement touch dragging
+
+ return (
+ this.slotDiv = slotDiv}>
+
+
{this._getMaxClassLabel(translate)}
+ {slotDetails}
+
+ {menu}
+
+ );
+ }
+
+
+ /**
+ * Toggle the modifications flag when selecting the modifications icon
+ */
+ _toggleModifications() {
+ this._modificationsSelected = !this._modificationsSelected;
+ }
+}
diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx
index 663a25b3..8e7f5075 100644
--- a/src/app/components/StandardSlot.jsx
+++ b/src/app/components/StandardSlot.jsx
@@ -1,161 +1,161 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import cn from 'classnames';
-import Persist from '../stores/Persist';
-import TranslatedComponent from './TranslatedComponent';
-import { diffDetails } from '../utils/SlotFunctions';
-import AvailableModulesMenu from './AvailableModulesMenu';
-import ModificationsMenu from './ModificationsMenu';
-import * as ModuleUtils from '../shipyard/ModuleUtils';
-import { ListModifications, Modified } from './SvgIcons';
-import { Modifications } from 'coriolis-data/dist';
-import { stopCtxPropagation } from '../utils/UtilityFunctions';
-import { blueprintTooltip } from '../utils/BlueprintFunctions';
-
-/**
- * Standard Slot
- */
-export default class StandardSlot extends TranslatedComponent {
-
- static propTypes = {
- slot: PropTypes.object,
- modules: PropTypes.array.isRequired,
- onSelect: PropTypes.func.isRequired,
- onOpen: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- ship: PropTypes.object.isRequired,
- selected: PropTypes.bool,
- warning: PropTypes.func,
- };
-
- /**
- * Construct the slot
- * @param {object} props Object properties
- */
- constructor(props) {
- super(props);
- this._modificationsSelected = false;
- this._keyDown = this._keyDown.bind(this);
- this.modButton = null;
- this.slotDiv = null;
- }
-
- /**
- * Fired on key down
- * @param {KeyboardEvent} event The keydown event
- * @private
- */
- _keyDown(event) {
- if (event.key == 'Enter') {
- if(event.target.className == 'r') {
- this._toggleModifications();
- }
- this.props.onOpen(event);
- }
- }
-
- /**
- * Render the slot
- * @return {React.Component} Slot component
- */
- render() {
- let { termtip, tooltip } = this.context;
- let { translate, formats, units } = this.context.language;
- 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.modules[m.grp] ? [] : (Modifications.modules[m.grp].modifications || []);
- if (m && m.name && m.name === 'Guardian Hybrid Power Plant') {
- validMods = [];
- }
- let showModuleResistances = Persist.showModuleResistances();
- let mass = m.getMass() || m.cargo || m.fuel || 0;
-
- // Modifications tooltip shows blueprint and grade, if available
- let modTT = translate('modified');
- if (m && m.blueprint && m.blueprint.name) {
- modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
- if (m.blueprint.special && m.blueprint.special.id >= 0) {
- modTT += ', ' + translate(m.blueprint.special.name);
- }
- modTT = (
-
-
{modTT}
- {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
-
- );
- }
-
- if (!selected) {
- // If not selected then sure that modifications flag is unset
- this._modificationsSelected = false;
- }
-
- const modificationsMarker = JSON.stringify(m);
-
- if (selected) {
- if (this._modificationsSelected) {
- menu = ;
- } else {
- menu = ;
- }
- }
-
- return (
- this.slotDiv = slotDiv }>
-
-
{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}
-
-
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
-
{formats.round(mass)}{units.T}
-
-
- { m.getMinMass() ?
{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}
: null }
- { m.getOptMass() ?
{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}
: null }
- { m.getMaxMass() ?
{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
: null }
- { m.getOptMul() ?
{translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}
: null }
- { m.getRange() ?
{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}
: null }
- { m.time ?
{translate('time')}: {formats.time(m.time)}
: 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 }
- { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null }
- { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null }
- { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null }
- { m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null }
- { validMods.length > 0 ?
this.modButton = modButton }>
: null }
-
-
-
- {menu}
-
- );
- }
-
- /**
- * Toggle the modifications flag when selecting the modifications icon
- */
- _toggleModifications() {
- this._modificationsSelected = !this._modificationsSelected;
- }
-}
+import React from 'react';
+import PropTypes from 'prop-types';
+import cn from 'classnames';
+import Persist from '../stores/Persist';
+import TranslatedComponent from './TranslatedComponent';
+import { diffDetails } from '../utils/SlotFunctions';
+import AvailableModulesMenu from './AvailableModulesMenu';
+import ModificationsMenu from './ModificationsMenu';
+import * as ModuleUtils from '../shipyard/ModuleUtils';
+import { ListModifications, Modified } from './SvgIcons';
+import { Modifications } from 'coriolis-data/dist';
+import { stopCtxPropagation } from '../utils/UtilityFunctions';
+import { blueprintTooltip } from '../utils/BlueprintFunctions';
+
+/**
+ * Standard Slot
+ */
+export default class StandardSlot extends TranslatedComponent {
+
+ static propTypes = {
+ slot: PropTypes.object,
+ modules: PropTypes.array.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onOpen: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ ship: PropTypes.object.isRequired,
+ selected: PropTypes.bool,
+ warning: PropTypes.func,
+ };
+
+ /**
+ * Construct the slot
+ * @param {object} props Object properties
+ */
+ constructor(props) {
+ super(props);
+ this._modificationsSelected = false;
+ this._keyDown = this._keyDown.bind(this);
+ this.modButton = null;
+ this.slotDiv = null;
+ }
+
+ /**
+ * Fired on key down
+ * @param {KeyboardEvent} event The keydown event
+ * @private
+ */
+ _keyDown(event) {
+ if (event.key == 'Enter') {
+ if(event.target.className == 'r') {
+ this._toggleModifications();
+ }
+ this.props.onOpen(event);
+ }
+ }
+
+ /**
+ * Render the slot
+ * @return {React.Component} Slot component
+ */
+ render() {
+ let { termtip, tooltip } = this.context;
+ let { translate, formats, units } = this.context.language;
+ 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.modules[m.grp] ? [] : (Modifications.modules[m.grp].modifications || []);
+ if (m && m.name && m.name === 'Guardian Hybrid Power Plant') {
+ validMods = [];
+ }
+ let showModuleResistances = Persist.showModuleResistances();
+ let mass = m.getMass() || m.cargo || m.fuel || 0;
+
+ // Modifications tooltip shows blueprint and grade, if available
+ let modTT = translate('modified');
+ if (m && m.blueprint && m.blueprint.name) {
+ modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
+ if (m.blueprint.special && m.blueprint.special.id >= 0) {
+ modTT += ', ' + translate(m.blueprint.special.name);
+ }
+ modTT = (
+
+
{modTT}
+ {blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
+
+ );
+ }
+
+ if (!selected) {
+ // If not selected then sure that modifications flag is unset
+ this._modificationsSelected = false;
+ }
+
+ const modificationsMarker = JSON.stringify(m);
+
+ if (selected) {
+ if (this._modificationsSelected) {
+ menu = ;
+ } else {
+ menu = ;
+ }
+ }
+
+ return (
+ this.slotDiv = slotDiv }>
+
+
{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}
+
+
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
+
{formats.round(mass)}{units.T}
+
+
+ { m.getMinMass() ?
{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}
: null }
+ { m.getOptMass() ?
{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}
: null }
+ { m.getMaxMass() ?
{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
: null }
+ { m.getOptMul() ?
{translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}
: null }
+ { m.getRange() ?
{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}
: null }
+ { m.time ?
{translate('time')}: {formats.time(m.time)}
: 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 }
+ { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null }
+ { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null }
+ { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null }
+ { m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null }
+ { validMods.length > 0 ?
this.modButton = modButton }>
: null }
+
+
+
+ {menu}
+
+ );
+ }
+
+ /**
+ * Toggle the modifications flag when selecting the modifications icon
+ */
+ _toggleModifications() {
+ this._modificationsSelected = !this._modificationsSelected;
+ }
+}
diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx
index afb7df89..a26efcc4 100644
--- a/src/app/pages/OutfittingPage.jsx
+++ b/src/app/pages/OutfittingPage.jsx
@@ -1,669 +1,669 @@
-import React from 'react';
-// import Perf from 'react-addons-perf';
-import { Ships } from 'coriolis-data/dist';
-import cn from 'classnames';
-import Page from './Page';
-import Router from '../Router';
-import Persist from '../stores/Persist';
-import * as Utils from '../utils/UtilityFunctions';
-import Ship from '../shipyard/Ship';
-import { toDetailedBuild } from '../shipyard/Serializer';
-import { outfitURL } from '../utils/UrlGenerators';
-import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons';
-import LZString from 'lz-string';
-import ShipSummaryTable from '../components/ShipSummaryTable';
-import StandardSlotSection from '../components/StandardSlotSection';
-import HardpointSlotSection from '../components/HardpointSlotSection';
-import InternalSlotSection from '../components/InternalSlotSection';
-import UtilitySlotSection from '../components/UtilitySlotSection';
-import Pips from '../components/Pips';
-import Boost from '../components/Boost';
-import Fuel from '../components/Fuel';
-import Cargo from '../components/Cargo';
-import ShipPicker from '../components/ShipPicker';
-import EngagementRange from '../components/EngagementRange';
-import OutfittingSubpages from '../components/OutfittingSubpages';
-import ModalExport from '../components/ModalExport';
-import ModalPermalink from '../components/ModalPermalink';
-
-/**
- * Document Title Generator
- * @param {String} shipName Ship Name
- * @param {String} buildName Build Name
- * @return {String} Document title
- */
-function getTitle(shipName, buildName) {
- return buildName ? buildName : shipName;
-}
-
-/**
- * The Outfitting Page
- */
-export default class OutfittingPage extends Page {
-
- /**
- * Constructor
- * @param {Object} props React Component properties
- * @param {Object} context React Component context
- */
- constructor(props, context) {
- super(props, context);
- // window.Perf = Perf;
- this.state = this._initState(props, context);
- this._keyDown = this._keyDown.bind(this);
- this._exportBuild = this._exportBuild.bind(this);
- this._pipsUpdated = this._pipsUpdated.bind(this);
- this._boostUpdated = this._boostUpdated.bind(this);
- this._cargoUpdated = this._cargoUpdated.bind(this);
- this._fuelUpdated = this._fuelUpdated.bind(this);
- this._opponentUpdated = this._opponentUpdated.bind(this);
- this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this);
- }
-
- /**
- * [Re]Create initial state from context
- * @param {Object} props React component properties
- * @param {context} context React component context
- * @return {Object} New state object
- */
- _initState(props, context) {
- let params = context.route.params;
- let shipId = params.ship;
- let code = params.code;
- let buildName = params.bn;
- let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
- let savedCode = Persist.getBuild(shipId, buildName);
- if (!data) {
- return { error: { message: 'Ship not found: ' + shipId } };
- }
- let ship = new Ship(shipId, data.properties, data.slots); // Create a new Ship instance
- if (code) {
- ship.buildFrom(code); // Populate modules from serialized 'code' URL param
- } else {
- ship.buildWith(data.defaults); // Populate with default components
- }
-
- this._getTitle = getTitle.bind(this, data.properties.name);
-
- // Obtain ship control from code
- const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code);
- return {
- error: null,
- title: this._getTitle(buildName),
- costTab: Persist.getCostTab() || 'costs',
- buildName,
- newBuildName: buildName,
- shipId,
- ship,
- code,
- savedCode,
- sys,
- eng,
- wep,
- boost,
- fuel,
- cargo,
- opponent,
- opponentBuild,
- opponentSys,
- opponentEng,
- opponentWep,
- engagementRange
- };
- }
-
- /**
- * Handle build name change and update state
- * @param {SyntheticEvent} event React Event
- */
- _buildNameChange(event) {
- let stateChanges = {
- newBuildName: event.target.value
- };
-
- if (Persist.hasBuild(this.state.shipId, stateChanges.newBuildName)) {
- stateChanges.savedCode = Persist.getBuild(this.state.shipId, stateChanges.newBuildName);
- } else {
- stateChanges.savedCode = null;
- }
-
- this.setState(stateChanges);
- }
-
- /**
- * Update the control part of the route
- */
- _updateRouteOnControlChange() {
- const { ship, shipId, buildName } = this.state;
- const code = this._fullCode(ship);
- this._updateRoute(shipId, buildName, code);
- this.setState({ code });
- }
-
- /**
- * Provide a full code for this ship, including any additions due to the outfitting page
- * @param {Object} ship the ship
- * @param {number} fuel the fuel carried by the ship (if different from that in state)
- * @param {number} cargo the cargo carried by the ship (if different from that in state)
- * @returns {string} the code for this ship
- */
- _fullCode(ship, fuel, cargo) {
- return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`;
- }
-
- /**
- * Obtain the control information from the build code
- * @param {Object} ship The ship
- * @param {string} code The build code
- * @returns {Object} The control information
- */
- _obtainControlFromCode(ship, code) {
- // Defaults
- let sys = 2;
- let eng = 2;
- let wep = 2;
- let boost = false;
- let fuel = ship.fuelCapacity;
- let cargo = ship.cargoCapacity;
- let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults);
- let opponentSys = 2;
- let opponentEng = 2;
- let opponentWep = 2;
- let opponentBuild;
- let engagementRange = 1000;
-
- // Obtain updates from code, if available
- if (code) {
- const parts = code.split('.');
- if (parts.length >= 5) {
- // We have control information in the code
- const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/');
- sys = parseFloat(control[0]);
- eng = parseFloat(control[1]);
- wep = parseFloat(control[2]);
- boost = control[3] == 1 ? true : false;
- fuel = parseFloat(control[4]);
- cargo = parseInt(control[5]);
- if (control[6]) {
- const shipId = control[6];
- opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
- if (control[7] && Persist.getBuild(shipId, control[7])) {
- // Ship is a particular build
- const opponentCode = Persist.getBuild(shipId, control[7]);
- opponent.buildFrom(opponentCode);
- opponentBuild = control[7];
- if (opponentBuild) {
- // Obtain opponent's sys/eng/wep pips from their code
- const opponentParts = opponentCode.split('.');
- if (opponentParts.length >= 5) {
- const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
- opponentSys = parseFloat(opponentControl[0]);
- opponentEng = parseFloat(opponentControl[1]);
- opponentWep = parseFloat(opponentControl[2]);
- }
- }
- } else {
- // Ship is a stock build
- opponent.buildWith(Ships[shipId].defaults);
- }
- }
- engagementRange = parseInt(control[8]);
- }
- }
-
- return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange };
- }
-
- /**
- * Triggered when pips have been updated
- * @param {number} sys SYS pips
- * @param {number} eng ENG pips
- * @param {number} wep WEP pips
- */
- _pipsUpdated(sys, eng, wep) {
- this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Triggered when boost has been updated
- * @param {boolean} boost true if boosting
- */
- _boostUpdated(boost) {
- this.setState({ boost }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Triggered when fuel has been updated
- * @param {number} fuel the amount of fuel, in T
- */
- _fuelUpdated(fuel) {
- this.setState({ fuel }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Triggered when cargo has been updated
- * @param {number} cargo the amount of cargo, in T
- */
- _cargoUpdated(cargo) {
- this.setState({ cargo }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Triggered when engagement range has been updated
- * @param {number} engagementRange the engagement range, in m
- */
- _engagementRangeUpdated(engagementRange) {
- this.setState({ engagementRange }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Triggered when target ship has been updated
- * @param {string} opponent the opponent's ship model
- * @param {string} opponentBuild the name of the opponent's build
- */
- _opponentUpdated(opponent, opponentBuild) {
- const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots);
- let opponentSys = this.state.opponentSys;
- let opponentEng = this.state.opponentEng;
- let opponentWep = this.state.opponentWep;
- if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) {
- // Ship is a particular build
- opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild));
- // Set pips for opponent
- const opponentParts = Persist.getBuild(opponent, opponentBuild).split('.');
- if (opponentParts.length >= 5) {
- const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
- opponentSys = parseFloat(opponentControl[0]);
- opponentEng = parseFloat(opponentControl[1]);
- opponentWep = parseFloat(opponentControl[2]);
- }
- } else {
- // Ship is a stock build
- opponentShip.buildWith(Ships[opponent].defaults);
- opponentSys = 2;
- opponentEng = 2;
- opponentWep = 2;
- }
-
- this.setState({ opponent: opponentShip, opponentBuild, opponentSys, opponentEng, opponentWep }, () => this._updateRouteOnControlChange());
- }
-
- /**
- * Set the control code for this outfitting page
- * @param {number} fuel the fuel carried by the ship (if different from that in state)
- * @param {number} cargo the cargo carried by the ship (if different from that in state)
- * @returns {string} The control code
- */
- _controlCode(fuel, cargo) {
- const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state;
- const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`;
- return code;
- }
-
- /**
- * Save the current build
- */
- _saveBuild() {
- const { ship, buildName, newBuildName, shipId } = this.state;
-
- // If this is a stock ship the code won't be set, so ensure that we have it
- const code = this.state.code || ship.toString();
-
- Persist.saveBuild(shipId, newBuildName, code);
- this._updateRoute(shipId, newBuildName, code);
-
- let opponent, opponentBuild, opponentSys, opponentEng, opponentWep;
- if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
- // This is a save of our current opponent build; update it
- opponentBuild = newBuildName;
- opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code);
- opponentSys = this.state.sys;
- opponentEng = this.state.eng;
- opponentWep = this.state.wep;
- } else {
- opponentBuild = this.state.opponentBuild;
- opponent = this.state.opponent;
- opponentSys = this.state.opponentSys;
- opponentEng = this.state.opponentEng;
- opponentWep = this.state.opponentWep;
- }
- this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) });
- }
-
- /**
- * Rename the current build
- */
- _renameBuild() {
- const { code, buildName, newBuildName, shipId, ship } = this.state;
- if (buildName != newBuildName && newBuildName.length) {
- Persist.deleteBuild(shipId, buildName);
- Persist.saveBuild(shipId, newBuildName, code);
- this._updateRoute(shipId, newBuildName, code);
- this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName });
- }
- }
-
- /**
- * Reload build from last save
- */
- _reloadBuild() {
- this.setState({ code: this.state.savedCode }, () => this._codeUpdated());
- }
-
- /**
- * Reset build to Stock/Factory defaults
- */
- _resetBuild() {
- const { ship, shipId, buildName } = this.state;
- // Rebuild ship
- ship.buildWith(Ships[shipId].defaults);
- // Reset controls
- const code = ship.toString();
- const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
- // Update state, and refresh the ship
- this.setState({
- sys,
- eng,
- wep,
- boost,
- fuel,
- cargo,
- opponent,
- opponentBuild,
- engagementRange
- }, () => this._updateRoute(shipId, buildName, code));
- }
-
- /**
- * Delete the build
- */
- _deleteBuild() {
- const { shipId, buildName } = this.state;
- Persist.deleteBuild(shipId, buildName);
-
- let opponentBuild;
- if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
- // Our current opponent has been deleted; revert to stock
- opponentBuild = null;
- } else {
- opponentBuild = this.state.opponentBuild;
- }
- Router.go(outfitURL(this.state.shipId));
-
- this.setState({ opponentBuild });
- }
-
- /**
- * Serialized and show the export modal
- */
- _exportBuild() {
- let translate = this.context.language.translate;
- let { buildName, ship } = this.state;
- this.context.showModal( );
- }
-
- /**
- * Called when the code for the ship has been updated, to synchronise the rest of the data
- */
- _codeUpdated() {
- const { code, ship, shipId, buildName } = this.state;
-
- // Rebuild ship from the code
- this.state.ship.buildFrom(code);
-
- // Obtain controls from the code
- const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
- // Update state, and refresh the route when complete
- this.setState({
- sys,
- eng,
- wep,
- boost,
- fuel,
- cargo,
- opponent,
- opponentBuild,
- engagementRange
- }, () => this._updateRoute(shipId, buildName, code));
- }
-
- /**
- * Called when the ship has been updated, to set the code and then update accordingly
- */
- _shipUpdated() {
- let { ship, shipId, buildName, cargo, fuel } = this.state;
- if (cargo > ship.cargoCapacity) {
- cargo = ship.cargoCapacity;
- }
- if (fuel > ship.fuelCapacity) {
- fuel = ship.fuelCapacity;
- }
- const code = this._fullCode(ship, fuel, cargo);
- // Only update the state if this really has been updated
- if (this.state.code != code || this.state.cargo != cargo || this.state.fuel != fuel) {
- this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code));
- }
- }
-
- /**
- * Update the current route based on build
- * @param {string} shipId Ship Id
- * @param {string} buildName Current build name
- * @param {string} code Serialized ship 'code'
- */
- _updateRoute(shipId, buildName, code) {
- Router.replace(outfitURL(shipId, code, buildName));
- }
-
- /**
- * Update state based on context changes
- * @param {Object} nextProps Incoming/Next properties
- * @param {Object} nextContext Incoming/Next conext
- */
- componentWillReceiveProps(nextProps, nextContext) {
- if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
- this.setState(this._initState(nextProps, nextContext));
- }
- }
-
- /**
- * Add listeners when about to mount
- */
- componentWillMount() {
- document.addEventListener('keydown', this._keyDown);
- }
-
- /**
- * Remove listeners on unmount
- */
- componentWillUnmount() {
- document.removeEventListener('keydown', this._keyDown);
- }
-
- /**
- * Generates the short URL
- */
- _genShortlink() {
- this.context.showModal();
- }
-
- /**
- * Open up a window for EDDB with a shopping list of our components
- */
- _eddbShoppingList() {
- const ship = this.state.ship;
-
- const shipId = Ships[ship.id].eddbID;
- // Provide unique list of non-PP module EDDB IDs
- const modIds = ship.internal.concat(ship.bulkheads, ship.standard, ship.hardpoints).filter(slot => slot !== null && slot.m !== null && !slot.m.pp).map(slot => slot.m.eddbID).filter((v, i, a) => a.indexOf(v) === i);
-
- // Open up the relevant URL
- window.open('https://eddb.io/station?s=' + shipId + '&m=' + modIds.join(','));
- }
-
- /**
- * Handle Key Down
- * @param {Event} e Keyboard Event
- */
- _keyDown(e) {
- // .keyCode will eventually be replaced with .key
- switch (e.keyCode) {
- case 69: // 'e'
- if (e.ctrlKey || e.metaKey) { // CTRL/CMD + e
- e.preventDefault();
- this._exportBuild();
- }
- break;
- }
- }
-
- /**
- * Render the Page
- * @return {React.Component} The page contents
- */
- renderPage() {
- let state = this.state,
- { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context,
- { translate, units, formats } = language,
- { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state,
- hide = tooltip.bind(null, null),
- menu = this.props.currentMenu,
- shipUpdated = this._shipUpdated,
- canSave = (newBuildName || buildName) && code !== savedCode,
- canRename = buildName && newBuildName && buildName != newBuildName,
- canReload = savedCode && canSave;
-
- // Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up
- code = ship.name + (code || '');
-
- // Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time
- const _sStr = ship.getStandardString();
- const _iStr = ship.getInternalString();
- const _hStr = ship.getHardpointsString();
- const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`;
- const _mStr = ship.getModificationsString();
-
- const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`;
- const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`;
- const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`;
- const boostMarker = `${ship.canBoost(cargo, fuel)}`;
- const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`;
-
- const requirements = Ships[ship.id].requirements;
- let requirementElements = [];
-
- /**
- * Render the requirements for a ship / etc
- * @param {string} className Class names
- * @param {string} textKey The key for translating
- * @param {String} tooltipTextKey Tooltip key
- */
- function renderRequirement(className, textKey, tooltipTextKey) {
- requirementElements.push();
- }
-
- if (requirements) {
- requirements.federationRank && renderRequirement('federation', 'federation rank ' + requirements.federationRank, 'federation rank required');
- requirements.empireRank && renderRequirement('empire', 'empire rank ' + requirements.empireRank, 'empire rank required');
- requirements.horizons && renderRequirement('horizons', 'horizons', 'horizons required');
- requirements.horizonsEarlyAdoption && renderRequirement('horizons', 'horizons early adoption', 'horizons early adoption required');
- }
-
- return (
-
-
-
{ship.name}
-
{requirementElements}
-
-
-
-
-
-
- a|
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Main tables */}
-
-
-
-
-
-
- {/* Control of ship and opponent */}
-
-
-
{translate('ship control')}
-
-
-
-
-
-
-
-
-
-
- { ship.cargoCapacity > 0 ? : null }
-
-
-
-
{translate('opponent')}
-
-
-
-
-
-
-
-
-
- {/* Tabbed subpages */}
-
-
- );
- }
-}
+import React from 'react';
+// import Perf from 'react-addons-perf';
+import { Ships } from 'coriolis-data/dist';
+import cn from 'classnames';
+import Page from './Page';
+import Router from '../Router';
+import Persist from '../stores/Persist';
+import * as Utils from '../utils/UtilityFunctions';
+import Ship from '../shipyard/Ship';
+import { toDetailedBuild } from '../shipyard/Serializer';
+import { outfitURL } from '../utils/UrlGenerators';
+import { FloppyDisk, Bin, Switch, Download, Reload, LinkIcon, ShoppingIcon } from '../components/SvgIcons';
+import LZString from 'lz-string';
+import ShipSummaryTable from '../components/ShipSummaryTable';
+import StandardSlotSection from '../components/StandardSlotSection';
+import HardpointSlotSection from '../components/HardpointSlotSection';
+import InternalSlotSection from '../components/InternalSlotSection';
+import UtilitySlotSection from '../components/UtilitySlotSection';
+import Pips from '../components/Pips';
+import Boost from '../components/Boost';
+import Fuel from '../components/Fuel';
+import Cargo from '../components/Cargo';
+import ShipPicker from '../components/ShipPicker';
+import EngagementRange from '../components/EngagementRange';
+import OutfittingSubpages from '../components/OutfittingSubpages';
+import ModalExport from '../components/ModalExport';
+import ModalPermalink from '../components/ModalPermalink';
+
+/**
+ * Document Title Generator
+ * @param {String} shipName Ship Name
+ * @param {String} buildName Build Name
+ * @return {String} Document title
+ */
+function getTitle(shipName, buildName) {
+ return buildName ? buildName : shipName;
+}
+
+/**
+ * The Outfitting Page
+ */
+export default class OutfittingPage extends Page {
+
+ /**
+ * Constructor
+ * @param {Object} props React Component properties
+ * @param {Object} context React Component context
+ */
+ constructor(props, context) {
+ super(props, context);
+ // window.Perf = Perf;
+ this.state = this._initState(props, context);
+ this._keyDown = this._keyDown.bind(this);
+ this._exportBuild = this._exportBuild.bind(this);
+ this._pipsUpdated = this._pipsUpdated.bind(this);
+ this._boostUpdated = this._boostUpdated.bind(this);
+ this._cargoUpdated = this._cargoUpdated.bind(this);
+ this._fuelUpdated = this._fuelUpdated.bind(this);
+ this._opponentUpdated = this._opponentUpdated.bind(this);
+ this._engagementRangeUpdated = this._engagementRangeUpdated.bind(this);
+ }
+
+ /**
+ * [Re]Create initial state from context
+ * @param {Object} props React component properties
+ * @param {context} context React component context
+ * @return {Object} New state object
+ */
+ _initState(props, context) {
+ let params = context.route.params;
+ let shipId = params.ship;
+ let code = params.code;
+ let buildName = params.bn;
+ let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
+ let savedCode = Persist.getBuild(shipId, buildName);
+ if (!data) {
+ return { error: { message: 'Ship not found: ' + shipId } };
+ }
+ let ship = new Ship(shipId, data.properties, data.slots); // Create a new Ship instance
+ if (code) {
+ ship.buildFrom(code); // Populate modules from serialized 'code' URL param
+ } else {
+ ship.buildWith(data.defaults); // Populate with default components
+ }
+
+ this._getTitle = getTitle.bind(this, data.properties.name);
+
+ // Obtain ship control from code
+ const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = this._obtainControlFromCode(ship, code);
+ return {
+ error: null,
+ title: this._getTitle(buildName),
+ costTab: Persist.getCostTab() || 'costs',
+ buildName,
+ newBuildName: buildName,
+ shipId,
+ ship,
+ code,
+ savedCode,
+ sys,
+ eng,
+ wep,
+ boost,
+ fuel,
+ cargo,
+ opponent,
+ opponentBuild,
+ opponentSys,
+ opponentEng,
+ opponentWep,
+ engagementRange
+ };
+ }
+
+ /**
+ * Handle build name change and update state
+ * @param {SyntheticEvent} event React Event
+ */
+ _buildNameChange(event) {
+ let stateChanges = {
+ newBuildName: event.target.value
+ };
+
+ if (Persist.hasBuild(this.state.shipId, stateChanges.newBuildName)) {
+ stateChanges.savedCode = Persist.getBuild(this.state.shipId, stateChanges.newBuildName);
+ } else {
+ stateChanges.savedCode = null;
+ }
+
+ this.setState(stateChanges);
+ }
+
+ /**
+ * Update the control part of the route
+ */
+ _updateRouteOnControlChange() {
+ const { ship, shipId, buildName } = this.state;
+ const code = this._fullCode(ship);
+ this._updateRoute(shipId, buildName, code);
+ this.setState({ code });
+ }
+
+ /**
+ * Provide a full code for this ship, including any additions due to the outfitting page
+ * @param {Object} ship the ship
+ * @param {number} fuel the fuel carried by the ship (if different from that in state)
+ * @param {number} cargo the cargo carried by the ship (if different from that in state)
+ * @returns {string} the code for this ship
+ */
+ _fullCode(ship, fuel, cargo) {
+ return `${ship.toString()}.${LZString.compressToBase64(this._controlCode(fuel, cargo))}`;
+ }
+
+ /**
+ * Obtain the control information from the build code
+ * @param {Object} ship The ship
+ * @param {string} code The build code
+ * @returns {Object} The control information
+ */
+ _obtainControlFromCode(ship, code) {
+ // Defaults
+ let sys = 2;
+ let eng = 2;
+ let wep = 2;
+ let boost = false;
+ let fuel = ship.fuelCapacity;
+ let cargo = ship.cargoCapacity;
+ let opponent = new Ship('eagle', Ships['eagle'].properties, Ships['eagle'].slots).buildWith(Ships['eagle'].defaults);
+ let opponentSys = 2;
+ let opponentEng = 2;
+ let opponentWep = 2;
+ let opponentBuild;
+ let engagementRange = 1000;
+
+ // Obtain updates from code, if available
+ if (code) {
+ const parts = code.split('.');
+ if (parts.length >= 5) {
+ // We have control information in the code
+ const control = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[4])).split('/');
+ sys = parseFloat(control[0]);
+ eng = parseFloat(control[1]);
+ wep = parseFloat(control[2]);
+ boost = control[3] == 1 ? true : false;
+ fuel = parseFloat(control[4]);
+ cargo = parseInt(control[5]);
+ if (control[6]) {
+ const shipId = control[6];
+ opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots);
+ if (control[7] && Persist.getBuild(shipId, control[7])) {
+ // Ship is a particular build
+ const opponentCode = Persist.getBuild(shipId, control[7]);
+ opponent.buildFrom(opponentCode);
+ opponentBuild = control[7];
+ if (opponentBuild) {
+ // Obtain opponent's sys/eng/wep pips from their code
+ const opponentParts = opponentCode.split('.');
+ if (opponentParts.length >= 5) {
+ const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
+ opponentSys = parseFloat(opponentControl[0]);
+ opponentEng = parseFloat(opponentControl[1]);
+ opponentWep = parseFloat(opponentControl[2]);
+ }
+ }
+ } else {
+ // Ship is a stock build
+ opponent.buildWith(Ships[shipId].defaults);
+ }
+ }
+ engagementRange = parseInt(control[8]);
+ }
+ }
+
+ return { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange };
+ }
+
+ /**
+ * Triggered when pips have been updated
+ * @param {number} sys SYS pips
+ * @param {number} eng ENG pips
+ * @param {number} wep WEP pips
+ */
+ _pipsUpdated(sys, eng, wep) {
+ this.setState({ sys, eng, wep }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Triggered when boost has been updated
+ * @param {boolean} boost true if boosting
+ */
+ _boostUpdated(boost) {
+ this.setState({ boost }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Triggered when fuel has been updated
+ * @param {number} fuel the amount of fuel, in T
+ */
+ _fuelUpdated(fuel) {
+ this.setState({ fuel }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Triggered when cargo has been updated
+ * @param {number} cargo the amount of cargo, in T
+ */
+ _cargoUpdated(cargo) {
+ this.setState({ cargo }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Triggered when engagement range has been updated
+ * @param {number} engagementRange the engagement range, in m
+ */
+ _engagementRangeUpdated(engagementRange) {
+ this.setState({ engagementRange }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Triggered when target ship has been updated
+ * @param {string} opponent the opponent's ship model
+ * @param {string} opponentBuild the name of the opponent's build
+ */
+ _opponentUpdated(opponent, opponentBuild) {
+ const opponentShip = new Ship(opponent, Ships[opponent].properties, Ships[opponent].slots);
+ let opponentSys = this.state.opponentSys;
+ let opponentEng = this.state.opponentEng;
+ let opponentWep = this.state.opponentWep;
+ if (opponentBuild && Persist.getBuild(opponent, opponentBuild)) {
+ // Ship is a particular build
+ opponentShip.buildFrom(Persist.getBuild(opponent, opponentBuild));
+ // Set pips for opponent
+ const opponentParts = Persist.getBuild(opponent, opponentBuild).split('.');
+ if (opponentParts.length >= 5) {
+ const opponentControl = LZString.decompressFromBase64(Utils.fromUrlSafe(opponentParts[4])).split('/');
+ opponentSys = parseFloat(opponentControl[0]);
+ opponentEng = parseFloat(opponentControl[1]);
+ opponentWep = parseFloat(opponentControl[2]);
+ }
+ } else {
+ // Ship is a stock build
+ opponentShip.buildWith(Ships[opponent].defaults);
+ opponentSys = 2;
+ opponentEng = 2;
+ opponentWep = 2;
+ }
+
+ this.setState({ opponent: opponentShip, opponentBuild, opponentSys, opponentEng, opponentWep }, () => this._updateRouteOnControlChange());
+ }
+
+ /**
+ * Set the control code for this outfitting page
+ * @param {number} fuel the fuel carried by the ship (if different from that in state)
+ * @param {number} cargo the cargo carried by the ship (if different from that in state)
+ * @returns {string} The control code
+ */
+ _controlCode(fuel, cargo) {
+ const { sys, eng, wep, boost, opponent, opponentBuild, engagementRange } = this.state;
+ const code = `${sys}/${eng}/${wep}/${boost ? 1 : 0}/${fuel || this.state.fuel}/${cargo || this.state.cargo}/${opponent.id}/${opponentBuild ? opponentBuild : ''}/${engagementRange}`;
+ return code;
+ }
+
+ /**
+ * Save the current build
+ */
+ _saveBuild() {
+ const { ship, buildName, newBuildName, shipId } = this.state;
+
+ // If this is a stock ship the code won't be set, so ensure that we have it
+ const code = this.state.code || ship.toString();
+
+ Persist.saveBuild(shipId, newBuildName, code);
+ this._updateRoute(shipId, newBuildName, code);
+
+ let opponent, opponentBuild, opponentSys, opponentEng, opponentWep;
+ if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
+ // This is a save of our current opponent build; update it
+ opponentBuild = newBuildName;
+ opponent = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots).buildFrom(code);
+ opponentSys = this.state.sys;
+ opponentEng = this.state.eng;
+ opponentWep = this.state.wep;
+ } else {
+ opponentBuild = this.state.opponentBuild;
+ opponent = this.state.opponent;
+ opponentSys = this.state.opponentSys;
+ opponentEng = this.state.opponentEng;
+ opponentWep = this.state.opponentWep;
+ }
+ this.setState({ buildName: newBuildName, code, savedCode: code, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, title: this._getTitle(newBuildName) });
+ }
+
+ /**
+ * Rename the current build
+ */
+ _renameBuild() {
+ const { code, buildName, newBuildName, shipId, ship } = this.state;
+ if (buildName != newBuildName && newBuildName.length) {
+ Persist.deleteBuild(shipId, buildName);
+ Persist.saveBuild(shipId, newBuildName, code);
+ this._updateRoute(shipId, newBuildName, code);
+ this.setState({ buildName: newBuildName, code, savedCode: code, opponentBuild: newBuildName });
+ }
+ }
+
+ /**
+ * Reload build from last save
+ */
+ _reloadBuild() {
+ this.setState({ code: this.state.savedCode }, () => this._codeUpdated());
+ }
+
+ /**
+ * Reset build to Stock/Factory defaults
+ */
+ _resetBuild() {
+ const { ship, shipId, buildName } = this.state;
+ // Rebuild ship
+ ship.buildWith(Ships[shipId].defaults);
+ // Reset controls
+ const code = ship.toString();
+ const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
+ // Update state, and refresh the ship
+ this.setState({
+ sys,
+ eng,
+ wep,
+ boost,
+ fuel,
+ cargo,
+ opponent,
+ opponentBuild,
+ engagementRange
+ }, () => this._updateRoute(shipId, buildName, code));
+ }
+
+ /**
+ * Delete the build
+ */
+ _deleteBuild() {
+ const { shipId, buildName } = this.state;
+ Persist.deleteBuild(shipId, buildName);
+
+ let opponentBuild;
+ if (shipId === this.state.opponent.id && buildName === this.state.opponentBuild) {
+ // Our current opponent has been deleted; revert to stock
+ opponentBuild = null;
+ } else {
+ opponentBuild = this.state.opponentBuild;
+ }
+ Router.go(outfitURL(this.state.shipId));
+
+ this.setState({ opponentBuild });
+ }
+
+ /**
+ * Serialized and show the export modal
+ */
+ _exportBuild() {
+ let translate = this.context.language.translate;
+ let { buildName, ship } = this.state;
+ this.context.showModal( );
+ }
+
+ /**
+ * Called when the code for the ship has been updated, to synchronise the rest of the data
+ */
+ _codeUpdated() {
+ const { code, ship, shipId, buildName } = this.state;
+
+ // Rebuild ship from the code
+ this.state.ship.buildFrom(code);
+
+ // Obtain controls from the code
+ const { sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, engagementRange } = this._obtainControlFromCode(ship, code);
+ // Update state, and refresh the route when complete
+ this.setState({
+ sys,
+ eng,
+ wep,
+ boost,
+ fuel,
+ cargo,
+ opponent,
+ opponentBuild,
+ engagementRange
+ }, () => this._updateRoute(shipId, buildName, code));
+ }
+
+ /**
+ * Called when the ship has been updated, to set the code and then update accordingly
+ */
+ _shipUpdated() {
+ let { ship, shipId, buildName, cargo, fuel } = this.state;
+ if (cargo > ship.cargoCapacity) {
+ cargo = ship.cargoCapacity;
+ }
+ if (fuel > ship.fuelCapacity) {
+ fuel = ship.fuelCapacity;
+ }
+ const code = this._fullCode(ship, fuel, cargo);
+ // Only update the state if this really has been updated
+ if (this.state.code != code || this.state.cargo != cargo || this.state.fuel != fuel) {
+ this.setState({ code, cargo, fuel }, () => this._updateRoute(shipId, buildName, code));
+ }
+ }
+
+ /**
+ * Update the current route based on build
+ * @param {string} shipId Ship Id
+ * @param {string} buildName Current build name
+ * @param {string} code Serialized ship 'code'
+ */
+ _updateRoute(shipId, buildName, code) {
+ Router.replace(outfitURL(shipId, code, buildName));
+ }
+
+ /**
+ * Update state based on context changes
+ * @param {Object} nextProps Incoming/Next properties
+ * @param {Object} nextContext Incoming/Next conext
+ */
+ componentWillReceiveProps(nextProps, nextContext) {
+ if (this.context.route !== nextContext.route) { // Only reinit state if the route has changed
+ this.setState(this._initState(nextProps, nextContext));
+ }
+ }
+
+ /**
+ * Add listeners when about to mount
+ */
+ componentWillMount() {
+ document.addEventListener('keydown', this._keyDown);
+ }
+
+ /**
+ * Remove listeners on unmount
+ */
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this._keyDown);
+ }
+
+ /**
+ * Generates the short URL
+ */
+ _genShortlink() {
+ this.context.showModal();
+ }
+
+ /**
+ * Open up a window for EDDB with a shopping list of our components
+ */
+ _eddbShoppingList() {
+ const ship = this.state.ship;
+
+ const shipId = Ships[ship.id].eddbID;
+ // Provide unique list of non-PP module EDDB IDs
+ const modIds = ship.internal.concat(ship.bulkheads, ship.standard, ship.hardpoints).filter(slot => slot !== null && slot.m !== null && !slot.m.pp).map(slot => slot.m.eddbID).filter((v, i, a) => a.indexOf(v) === i);
+
+ // Open up the relevant URL
+ window.open('https://eddb.io/station?s=' + shipId + '&m=' + modIds.join(','));
+ }
+
+ /**
+ * Handle Key Down
+ * @param {Event} e Keyboard Event
+ */
+ _keyDown(e) {
+ // .keyCode will eventually be replaced with .key
+ switch (e.keyCode) {
+ case 69: // 'e'
+ if (e.ctrlKey || e.metaKey) { // CTRL/CMD + e
+ e.preventDefault();
+ this._exportBuild();
+ }
+ break;
+ }
+ }
+
+ /**
+ * Render the Page
+ * @return {React.Component} The page contents
+ */
+ renderPage() {
+ let state = this.state,
+ { language, termtip, tooltip, sizeRatio, onWindowResize } = this.context,
+ { translate, units, formats } = language,
+ { ship, code, savedCode, buildName, newBuildName, sys, eng, wep, boost, fuel, cargo, opponent, opponentBuild, opponentSys, opponentEng, opponentWep, engagementRange } = state,
+ hide = tooltip.bind(null, null),
+ menu = this.props.currentMenu,
+ shipUpdated = this._shipUpdated,
+ canSave = (newBuildName || buildName) && code !== savedCode,
+ canRename = buildName && newBuildName && buildName != newBuildName,
+ canReload = savedCode && canSave;
+
+ // Code can be blank for a default loadout. Prefix it with the ship name to ensure that changes in default ships is picked up
+ code = ship.name + (code || '');
+
+ // Markers are used to propagate state changes without requiring a deep comparison of the ship, as that takes a long time
+ const _sStr = ship.getStandardString();
+ const _iStr = ship.getInternalString();
+ const _hStr = ship.getHardpointsString();
+ const _pStr = `${ship.getPowerEnabledString()}${ship.getPowerPrioritiesString()}`;
+ const _mStr = ship.getModificationsString();
+
+ const standardSlotMarker = `${ship.name}${_sStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`;
+ const internalSlotMarker = `${ship.name}${_iStr}${_pStr}${_mStr}`;
+ const hardpointsSlotMarker = `${ship.name}${_hStr}${_pStr}${_mStr}`;
+ const boostMarker = `${ship.canBoost(cargo, fuel)}`;
+ const shipSummaryMarker = `${ship.name}${_sStr}${_iStr}${_hStr}${_pStr}${_mStr}${ship.ladenMass}${cargo}${fuel}`;
+
+ const requirements = Ships[ship.id].requirements;
+ let requirementElements = [];
+
+ /**
+ * Render the requirements for a ship / etc
+ * @param {string} className Class names
+ * @param {string} textKey The key for translating
+ * @param {String} tooltipTextKey Tooltip key
+ */
+ function renderRequirement(className, textKey, tooltipTextKey) {
+ requirementElements.push();
+ }
+
+ if (requirements) {
+ requirements.federationRank && renderRequirement('federation', 'federation rank ' + requirements.federationRank, 'federation rank required');
+ requirements.empireRank && renderRequirement('empire', 'empire rank ' + requirements.empireRank, 'empire rank required');
+ requirements.horizons && renderRequirement('horizons', 'horizons', 'horizons required');
+ requirements.horizonsEarlyAdoption && renderRequirement('horizons', 'horizons early adoption', 'horizons early adoption required');
+ }
+
+ return (
+
+
+
{ship.name}
+
{requirementElements}
+
+
+
+
+
+
+ a|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Main tables */}
+
+
+
+
+
+
+ {/* Control of ship and opponent */}
+
+
+
{translate('ship control')}
+
+
+
+
+
+
+
+
+
+
+ { ship.cargoCapacity > 0 ? : null }
+
+
+
+
{translate('opponent')}
+
+
+
+
+
+
+
+
+
+ {/* Tabbed subpages */}
+
+
+ );
+ }
+}
diff --git a/src/app/pages/ShipyardPage.jsx b/src/app/pages/ShipyardPage.jsx
index 842ca5bf..5f5e1842 100644
--- a/src/app/pages/ShipyardPage.jsx
+++ b/src/app/pages/ShipyardPage.jsx
@@ -396,4 +396,4 @@ export default class ShipyardPage extends Page {
);
}
-}
\ No newline at end of file
+}
diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js
index a597dae7..c57c6835 100644
--- a/src/app/utils/BlueprintFunctions.js
+++ b/src/app/utils/BlueprintFunctions.js
@@ -1,430 +1,430 @@
-import React from 'react';
-import { Modifications } from 'coriolis-data/dist';
-
-/**
- * Generate a tooltip with details of a blueprint's specials
- * @param {Object} translate The translate object
- * @param {Object} blueprint The blueprint at the required grade
- * @param {string} grp The group of the module
- * @param {Object} m The module to compare with
- * @param {string} specialName The name of the special
- * @returns {Object} The react components
- */
-export function specialToolTip(translate, blueprint, grp, m, specialName) {
- const effects = [];
- if (!blueprint || !blueprint.features) {
- return undefined;
- }
- if (m) {
- // We also add in any benefits from specials that aren't covered above
- if (m.blueprint) {
- for (const feature in Modifications.modifierActions[specialName]) {
- // if (!blueprint.features[feature] && !m.mods.feature) {
- const featureDef = Modifications.modifications[feature];
- if (featureDef && !featureDef.hidden) {
- let symbol = '';
- if (feature === 'jitter') {
- symbol = '°';
- } else if (featureDef.type === 'percentage') {
- symbol = '%';
- }
- let current = m.getModValue(feature) - m.getModValue(feature, true);
- if (featureDef.type === 'percentage') {
- current = Math.round(current / 10) / 10;
- } else if (featureDef.type === 'numeric') {
- current /= 100;
- }
- const currentIsBeneficial = isValueBeneficial(feature, current);
-
- effects.push(
-
- {translate(feature, grp)}
-
- {current}{symbol}
-
-
- );
- }
- }
- }
- }
-
- return (
-
- );
-}
-
-/**
- * Generate a tooltip with details of a blueprint's effects
- * @param {Object} translate The translate object
- * @param {Object} blueprint The blueprint at the required grade
- * @param {Array} engineers The engineers supplying this blueprint
- * @param {string} grp The group of the module
- * @param {Object} m The module to compare with
- * @returns {Object} The react components
- */
-export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
- const effects = [];
- if (!blueprint || !blueprint.features) {
- return undefined;
- }
- for (const feature in blueprint.features) {
- const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
- const featureDef = Modifications.modifications[feature];
- if (!featureDef.hidden) {
- let symbol = '';
- if (feature === 'jitter') {
- symbol = '°';
- } else if (featureDef.type === 'percentage') {
- symbol = '%';
- }
- let lowerBound = blueprint.features[feature][0];
- let upperBound = blueprint.features[feature][1];
- if (featureDef.type === 'percentage') {
- lowerBound = Math.round(lowerBound * 1000) / 10;
- upperBound = Math.round(upperBound * 1000) / 10;
- }
- const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
- const upperIsBeneficial = isValueBeneficial(feature, upperBound);
- if (m) {
- // We have a module - add in the current value
- let current = m.getModValue(feature);
- if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
- current = Math.round(current / 10) / 10;
- } else if (featureDef.type === 'numeric') {
- current /= 100;
- }
- const currentIsBeneficial = isValueBeneficial(feature, current);
- effects.push(
-
- {translate(feature, grp)}
- {lowerBound}{symbol}
- {current}{symbol}
- {upperBound}{symbol}
-
- );
- } else {
- // We do not have a module, no value
- effects.push(
-
- {translate(feature, grp)}
- {lowerBound}{symbol}
- {upperBound}{symbol}
-
- );
- }
- }
- }
- if (m) {
- // Because we have a module add in any benefits that aren't part of the primary blueprint
- for (const feature in m.mods) {
- if (!blueprint.features[feature]) {
- const featureDef = Modifications.modifications[feature];
- if (featureDef && !featureDef.hidden) {
- let symbol = '';
- if (feature === 'jitter') {
- symbol = '°';
- } else if (featureDef.type === 'percentage') {
- symbol = '%';
- }
- let current = m.getModValue(feature);
- if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
- current = Math.round(current / 10) / 10;
- } else if (featureDef.type === 'numeric') {
- current /= 100;
- }
- const currentIsBeneficial = isValueBeneficial(feature, current);
- effects.push(
-
- {translate(feature, grp)}
-
- {current}{symbol}
-
-
- );
- }
- }
- }
-
- // We also add in any benefits from specials that aren't covered above
- if (m.blueprint && m.blueprint.special) {
- for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
- if (!blueprint.features[feature] && !m.mods.feature) {
- const featureDef = Modifications.modifications[feature];
- if (featureDef && !featureDef.hidden) {
- let symbol = '';
- if (feature === 'jitter') {
- symbol = '°';
- } else if (featureDef.type === 'percentage') {
- symbol = '%';
- }
- let current = m.getModValue(feature);
- if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
- current = Math.round(current / 10) / 10;
- } else if (featureDef.type === 'numeric') {
- current /= 100;
- }
- const currentIsBeneficial = isValueBeneficial(feature, current);
- effects.push(
-
- {translate(feature, grp)}
-
- {current}{symbol}
-
-
- );
- }
- }
- }
- }
- }
-
- let components;
- if (!m) {
- components = [];
- for (const component in blueprint.components) {
- components.push(
-
- {translate(component)}
- {blueprint.components[component]}
-
- );
- }
- }
-
- let engineersList;
- if (engineers) {
- engineersList = [];
- for (const engineer of engineers) {
- engineersList.push(
-
- {engineer}
-
- );
- }
- }
-
- return (
-
-
-
-
- {translate('feature')}
- {translate('worst')}
- {m ? {translate('current')} : null }
- {translate('best')}
-
-
-
- {effects}
-
-
- { components ?
-
-
- {translate('component')}
- {translate('amount')}
-
-
-
- {components}
-
-
: null }
- { engineersList ?
-
-
- {translate('engineers')}
-
-
-
- {engineersList}
-
-
: null }
-
- );
-}
-
-/**
- * Is this blueprint feature beneficial?
- * @param {string} feature The name of the feature
- * @param {array} values The value of the feature
- * @returns {boolean} True if this feature is beneficial
- */
-export function isBeneficial(feature, values) {
- const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
- if (Modifications.modifications[feature].higherbetter) {
- return !fact;
- } else {
- return fact;
- }
-}
-
-/**
- * Is this feature value beneficial?
- * @param {string} feature The name of the feature
- * @param {number} value The value of the feature
- * @returns {boolean} True if this value is beneficial
- */
-export function isValueBeneficial(feature, value) {
- if (Modifications.modifications[feature].higherbetter) {
- return value > 0;
- } else {
- return value < 0;
- }
-}
-
-/**
- * Get a blueprint with a given name and an optional module
- * @param {string} name The name of the blueprint
- * @param {Object} module The module for which to obtain this blueprint
- * @returns {Object} The matching blueprint
- */
-export function getBlueprint(name, module) {
- // Start with a copy of the blueprint
- const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0);
- const found = Modifications.blueprints[findMod(name)];
- if (!found || !found.fdname) {
- return {};
- }
- const blueprint = JSON.parse(JSON.stringify(found));
- if (module) {
- if (module.grp === 'sb') {
- // Shield boosters are treated internally as straight modifiers, so rather than (for example)
- // being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look
- // accurate as per the information in Elite
- for (const grade in blueprint.grades) {
- for (const feature in blueprint.grades[grade].features) {
- if (feature === 'shieldboost') {
- blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
- blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
- }
- }
- }
- }
- }
- return blueprint;
-}
-
-/**
- * Provide 'percent' primary modifications
- * @param {Object} ship The ship for which to perform the modifications
- * @param {Object} m The module for which to perform the modifications
- * @param {Number} percent The percent to set values to of full.
- */
-export function setPercent(ship, m, percent) {
- ship.clearModifications(m);
- // Pick given value as multiplier
- const mult = percent / 100;
- const features = m.blueprint.grades[m.blueprint.grade].features;
- for (const featureName in features) {
- let value;
- if (Modifications.modifications[featureName].higherbetter) {
- // Higher is better, but is this making it better or worse?
- if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
- value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
- } else {
- value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
- }
- } else {
- // Higher is worse, but is this making it better or worse?
- if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
- value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
- } else {
- value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
- }
- }
-
- _setValue(ship, m, featureName, value);
- }
-}
-
-/**
- * Provide 'random' primary modifications
- * @param {Object} ship The ship for which to perform the modifications
- * @param {Object} m The module for which to perform the modifications
- */
-export function setRandom(ship, m) {
- // Pick a single value for our randomness
- setPercent(ship, m, Math.random() * 100);
-}
-
-/**
- * Set a modification feature value
- * @param {Object} ship The ship for which to perform the modifications
- * @param {Object} m The module for which to perform the modifications
- * @param {string} featureName The feature being set
- * @param {number} value The value being set for the feature
- */
-function _setValue(ship, m, featureName, value) {
- if (Modifications.modifications[featureName].type == 'percentage') {
- ship.setModification(m, featureName, value * 10000);
- } else if (Modifications.modifications[featureName].type == 'numeric') {
- ship.setModification(m, featureName, value * 100);
- } else {
- ship.setModification(m, featureName, value);
- }
-}
-
-/**
- * Provide 'percent' primary query
- * @param {Object} m The module for which to perform the query
- * @returns {Number} percent The percentage indicator of current applied values.
- */
-export function getPercent(m) {
- let result = null;
- const features = m.blueprint.grades[m.blueprint.grade].features;
- for (const featureName in features) {
- if (features[featureName][0] === features[featureName][1]) {
- continue;
- }
-
- let value = _getValue(m, featureName);
- let mult;
- if (Modifications.modifications[featureName].higherbetter) {
- // Higher is better, but is this making it better or worse?
- if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
- mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
- } else {
- mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
- }
- } else {
- // Higher is worse, but is this making it better or worse?
- if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
- mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
- } else {
- mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
- }
- }
-
- if (result && result != mult) {
- return null;
- } else if (result != mult) {
- result = mult;
- }
- }
-
- return result;
-}
-
-/**
- * Query a feature value
- * @param {Object} m The module for which to perform the query
- * @param {string} featureName The feature being queried
- * @returns {number} The value of the modification as a %
- */
-function _getValue(m, featureName) {
- if (Modifications.modifications[featureName].type == 'percentage') {
- return m.getModValue(featureName, true) / 10000;
- } else if (Modifications.modifications[featureName].type == 'numeric') {
- return m.getModValue(featureName, true) / 100;
- } else {
- return m.getModValue(featureName, true);
- }
-}
+import React from 'react';
+import { Modifications } from 'coriolis-data/dist';
+
+/**
+ * Generate a tooltip with details of a blueprint's specials
+ * @param {Object} translate The translate object
+ * @param {Object} blueprint The blueprint at the required grade
+ * @param {string} grp The group of the module
+ * @param {Object} m The module to compare with
+ * @param {string} specialName The name of the special
+ * @returns {Object} The react components
+ */
+export function specialToolTip(translate, blueprint, grp, m, specialName) {
+ const effects = [];
+ if (!blueprint || !blueprint.features) {
+ return undefined;
+ }
+ if (m) {
+ // We also add in any benefits from specials that aren't covered above
+ if (m.blueprint) {
+ for (const feature in Modifications.modifierActions[specialName]) {
+ // if (!blueprint.features[feature] && !m.mods.feature) {
+ const featureDef = Modifications.modifications[feature];
+ if (featureDef && !featureDef.hidden) {
+ let symbol = '';
+ if (feature === 'jitter') {
+ symbol = '°';
+ } else if (featureDef.type === 'percentage') {
+ symbol = '%';
+ }
+ let current = m.getModValue(feature) - m.getModValue(feature, true);
+ if (featureDef.type === 'percentage') {
+ current = Math.round(current / 10) / 10;
+ } else if (featureDef.type === 'numeric') {
+ current /= 100;
+ }
+ const currentIsBeneficial = isValueBeneficial(feature, current);
+
+ effects.push(
+
+ {translate(feature, grp)}
+
+ {current}{symbol}
+
+
+ );
+ }
+ }
+ }
+ }
+
+ return (
+
+ );
+}
+
+/**
+ * Generate a tooltip with details of a blueprint's effects
+ * @param {Object} translate The translate object
+ * @param {Object} blueprint The blueprint at the required grade
+ * @param {Array} engineers The engineers supplying this blueprint
+ * @param {string} grp The group of the module
+ * @param {Object} m The module to compare with
+ * @returns {Object} The react components
+ */
+export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
+ const effects = [];
+ if (!blueprint || !blueprint.features) {
+ return undefined;
+ }
+ for (const feature in blueprint.features) {
+ const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
+ const featureDef = Modifications.modifications[feature];
+ if (!featureDef.hidden) {
+ let symbol = '';
+ if (feature === 'jitter') {
+ symbol = '°';
+ } else if (featureDef.type === 'percentage') {
+ symbol = '%';
+ }
+ let lowerBound = blueprint.features[feature][0];
+ let upperBound = blueprint.features[feature][1];
+ if (featureDef.type === 'percentage') {
+ lowerBound = Math.round(lowerBound * 1000) / 10;
+ upperBound = Math.round(upperBound * 1000) / 10;
+ }
+ const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
+ const upperIsBeneficial = isValueBeneficial(feature, upperBound);
+ if (m) {
+ // We have a module - add in the current value
+ let current = m.getModValue(feature);
+ if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
+ current = Math.round(current / 10) / 10;
+ } else if (featureDef.type === 'numeric') {
+ current /= 100;
+ }
+ const currentIsBeneficial = isValueBeneficial(feature, current);
+ effects.push(
+
+ {translate(feature, grp)}
+ {lowerBound}{symbol}
+ {current}{symbol}
+ {upperBound}{symbol}
+
+ );
+ } else {
+ // We do not have a module, no value
+ effects.push(
+
+ {translate(feature, grp)}
+ {lowerBound}{symbol}
+ {upperBound}{symbol}
+
+ );
+ }
+ }
+ }
+ if (m) {
+ // Because we have a module add in any benefits that aren't part of the primary blueprint
+ for (const feature in m.mods) {
+ if (!blueprint.features[feature]) {
+ const featureDef = Modifications.modifications[feature];
+ if (featureDef && !featureDef.hidden) {
+ let symbol = '';
+ if (feature === 'jitter') {
+ symbol = '°';
+ } else if (featureDef.type === 'percentage') {
+ symbol = '%';
+ }
+ let current = m.getModValue(feature);
+ if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
+ current = Math.round(current / 10) / 10;
+ } else if (featureDef.type === 'numeric') {
+ current /= 100;
+ }
+ const currentIsBeneficial = isValueBeneficial(feature, current);
+ effects.push(
+
+ {translate(feature, grp)}
+
+ {current}{symbol}
+
+
+ );
+ }
+ }
+ }
+
+ // We also add in any benefits from specials that aren't covered above
+ if (m.blueprint && m.blueprint.special) {
+ for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
+ if (!blueprint.features[feature] && !m.mods.feature) {
+ const featureDef = Modifications.modifications[feature];
+ if (featureDef && !featureDef.hidden) {
+ let symbol = '';
+ if (feature === 'jitter') {
+ symbol = '°';
+ } else if (featureDef.type === 'percentage') {
+ symbol = '%';
+ }
+ let current = m.getModValue(feature);
+ if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
+ current = Math.round(current / 10) / 10;
+ } else if (featureDef.type === 'numeric') {
+ current /= 100;
+ }
+ const currentIsBeneficial = isValueBeneficial(feature, current);
+ effects.push(
+
+ {translate(feature, grp)}
+
+ {current}{symbol}
+
+
+ );
+ }
+ }
+ }
+ }
+ }
+
+ let components;
+ if (!m) {
+ components = [];
+ for (const component in blueprint.components) {
+ components.push(
+
+ {translate(component)}
+ {blueprint.components[component]}
+
+ );
+ }
+ }
+
+ let engineersList;
+ if (engineers) {
+ engineersList = [];
+ for (const engineer of engineers) {
+ engineersList.push(
+
+ {engineer}
+
+ );
+ }
+ }
+
+ return (
+
+
+
+
+ {translate('feature')}
+ {translate('worst')}
+ {m ? {translate('current')} : null }
+ {translate('best')}
+
+
+
+ {effects}
+
+
+ { components ?
+
+
+ {translate('component')}
+ {translate('amount')}
+
+
+
+ {components}
+
+
: null }
+ { engineersList ?
+
+
+ {translate('engineers')}
+
+
+
+ {engineersList}
+
+
: null }
+
+ );
+}
+
+/**
+ * Is this blueprint feature beneficial?
+ * @param {string} feature The name of the feature
+ * @param {array} values The value of the feature
+ * @returns {boolean} True if this feature is beneficial
+ */
+export function isBeneficial(feature, values) {
+ const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
+ if (Modifications.modifications[feature].higherbetter) {
+ return !fact;
+ } else {
+ return fact;
+ }
+}
+
+/**
+ * Is this feature value beneficial?
+ * @param {string} feature The name of the feature
+ * @param {number} value The value of the feature
+ * @returns {boolean} True if this value is beneficial
+ */
+export function isValueBeneficial(feature, value) {
+ if (Modifications.modifications[feature].higherbetter) {
+ return value > 0;
+ } else {
+ return value < 0;
+ }
+}
+
+/**
+ * Get a blueprint with a given name and an optional module
+ * @param {string} name The name of the blueprint
+ * @param {Object} module The module for which to obtain this blueprint
+ * @returns {Object} The matching blueprint
+ */
+export function getBlueprint(name, module) {
+ // Start with a copy of the blueprint
+ const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0);
+ const found = Modifications.blueprints[findMod(name)];
+ if (!found || !found.fdname) {
+ return {};
+ }
+ const blueprint = JSON.parse(JSON.stringify(found));
+ if (module) {
+ if (module.grp === 'sb') {
+ // Shield boosters are treated internally as straight modifiers, so rather than (for example)
+ // being a 4% boost they are a 104% multiplier. We need to fix the values here so that they look
+ // accurate as per the information in Elite
+ for (const grade in blueprint.grades) {
+ for (const feature in blueprint.grades[grade].features) {
+ if (feature === 'shieldboost') {
+ blueprint.grades[grade].features[feature][0] = ((1 + blueprint.grades[grade].features[feature][0]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
+ blueprint.grades[grade].features[feature][1] = ((1 + blueprint.grades[grade].features[feature][1]) * (1 + module.shieldboost) - 1) / module.shieldboost - 1;
+ }
+ }
+ }
+ }
+ }
+ return blueprint;
+}
+
+/**
+ * Provide 'percent' primary modifications
+ * @param {Object} ship The ship for which to perform the modifications
+ * @param {Object} m The module for which to perform the modifications
+ * @param {Number} percent The percent to set values to of full.
+ */
+export function setPercent(ship, m, percent) {
+ ship.clearModifications(m);
+ // Pick given value as multiplier
+ const mult = percent / 100;
+ const features = m.blueprint.grades[m.blueprint.grade].features;
+ for (const featureName in features) {
+ let value;
+ if (Modifications.modifications[featureName].higherbetter) {
+ // Higher is better, but is this making it better or worse?
+ if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
+ value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
+ } else {
+ value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
+ }
+ } else {
+ // Higher is worse, but is this making it better or worse?
+ if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
+ value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
+ } else {
+ value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
+ }
+ }
+
+ _setValue(ship, m, featureName, value);
+ }
+}
+
+/**
+ * Provide 'random' primary modifications
+ * @param {Object} ship The ship for which to perform the modifications
+ * @param {Object} m The module for which to perform the modifications
+ */
+export function setRandom(ship, m) {
+ // Pick a single value for our randomness
+ setPercent(ship, m, Math.random() * 100);
+}
+
+/**
+ * Set a modification feature value
+ * @param {Object} ship The ship for which to perform the modifications
+ * @param {Object} m The module for which to perform the modifications
+ * @param {string} featureName The feature being set
+ * @param {number} value The value being set for the feature
+ */
+function _setValue(ship, m, featureName, value) {
+ if (Modifications.modifications[featureName].type == 'percentage') {
+ ship.setModification(m, featureName, value * 10000);
+ } else if (Modifications.modifications[featureName].type == 'numeric') {
+ ship.setModification(m, featureName, value * 100);
+ } else {
+ ship.setModification(m, featureName, value);
+ }
+}
+
+/**
+ * Provide 'percent' primary query
+ * @param {Object} m The module for which to perform the query
+ * @returns {Number} percent The percentage indicator of current applied values.
+ */
+export function getPercent(m) {
+ let result = null;
+ const features = m.blueprint.grades[m.blueprint.grade].features;
+ for (const featureName in features) {
+ if (features[featureName][0] === features[featureName][1]) {
+ continue;
+ }
+
+ let value = _getValue(m, featureName);
+ let mult;
+ if (Modifications.modifications[featureName].higherbetter) {
+ // Higher is better, but is this making it better or worse?
+ if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
+ mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
+ } else {
+ mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
+ }
+ } else {
+ // Higher is worse, but is this making it better or worse?
+ if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
+ mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
+ } else {
+ mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
+ }
+ }
+
+ if (result && result != mult) {
+ return null;
+ } else if (result != mult) {
+ result = mult;
+ }
+ }
+
+ return result;
+}
+
+/**
+ * Query a feature value
+ * @param {Object} m The module for which to perform the query
+ * @param {string} featureName The feature being queried
+ * @returns {number} The value of the modification as a %
+ */
+function _getValue(m, featureName) {
+ if (Modifications.modifications[featureName].type == 'percentage') {
+ return m.getModValue(featureName, true) / 10000;
+ } else if (Modifications.modifications[featureName].type == 'numeric') {
+ return m.getModValue(featureName, true) / 100;
+ } else {
+ return m.getModValue(featureName, true);
+ }
+}