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 { CoriolisLogo, MountFixed, MountGimballed, MountTurret } from './SvgIcons'; import FuzzySearch from 'react-fuzzy'; 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', 'mlc': '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', 'advmc': 'projectiles', 'axmc': 'experimental', 'axmce': 'experimental', 'ntp': 'experimental', 'fc': 'projectiles', 'rfl': 'experimental', 'pa': 'projectiles', 'rg': 'projectiles', 'mr': 'ordnance', 'amr': 'ordnance', 'axmr': 'experimental', 'axmre': 'experimental', 'rcpl': 'experimental', 'dtl': 'experimental', 'tbsc': 'experimental', 'tbem': 'experimental', 'tbrfl': 'experimental', 'mahr': 'experimental', 'rsl': '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', // Guardian 'gpp': 'guardian', 'gpc': 'guardian', 'gsrp': 'guardian', 'ggc': 'guardian', 'gfsb': 'guardian', 'gmrp': 'guardian', 'gsc': 'guardian', 'ghrp': 'guardian', // Mining 'scl': 'mining', 'pwa': 'mining', 'sdm': 'mining', // Assists 'dc': 'flight assists', 'sua': 'flight assists', // Stabilizers 'ews': 'weapon stabilizers', }; // 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', 'mlc'], 'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'], 'rf': ['rf'], 'shields': ['sg', 'bsg', 'psg', 'scb'], 'structural reinforcement': ['hr', 'mrp'], 'flight assists': ['dc', 'sua'], // Hardpoints 'lasers': ['pl', 'ul', 'bl'], 'projectiles': ['mc', 'advmc', 'c', 'fc', 'pa', 'rg'], 'ordnance': ['mr', 'amr', 'tp', 'nl'], // Utilities 'sb': ['sb'], 'hs': ['hs'], 'csl': ['csl'], 'defence': ['ch', 'po', 'ec'], 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners // Experimental 'experimental': ['axmc', 'axmce', 'axmr', 'axmre', 'ntp','rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',], 'weapon stabilizers': ['ews'], // Guardian 'guardian': ['gpp', 'gpd', 'gpc', 'ggc', 'gsrp', 'gfsb', 'ghrp', 'gmrp', 'gsc'], 'mining': ['ml', 'scl', 'pwa', 'sdm', 'abl'], }; /** * 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, ship: PropTypes.object.isRequired, warning: PropTypes.func, firstSlotId: PropTypes.string, lastSlotId: PropTypes.string, activeSlotId: PropTypes.string, slotDiv: PropTypes.object }; /** * 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._showSearch = this._showSearch.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, onSelect, modules, ship } = props; let list, currentGroup; let buildGroup = this._buildGroup.bind( this, ship, translate, m, warning, (m, event) => { this._hideDiff(event); onSelect(m); } ); let fuzzy = []; 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) { // If this is a missing module/weapon, skip it if (m.grp == "mh" || m.grp == "mm"){ continue; } else { list.push(
    this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}
    ); } } else { if (category == "mh" || category == "mm"){ continue; } else { list.push(
    {translate(category)}
    ); } } } else { // Show category header as well as group header if (!categoryHeader) { if (category == "mh" || category == "mm"){ continue; } else { 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])); for (const i of modules[grp]) { let mount = ''; if (i.mount === 'F') { mount = 'Fixed'; } else if (i.mount === 'G') { mount = 'Gimballed'; } else if (i.mount === 'T') { mount = 'Turreted'; } let special = ''; if (typeof(i.special) !== 'undefined') { special = `(${translate(i.special)})`; } const fuzz = { grp, m: i, name: `${i.class}${i.rating}${mount ? ' ' + mount : ''} ${translate(grp)} ${translate(special)}` }; fuzzy.push(fuzz); } } } } } } let trackingFocus = false; return { list, currentGroup, fuzzy, trackingFocus }; } /** * Return Is expiremental capacity reached * @return {boolean} Is experimental capacity reached */ _experimentalCapacityReached() { const ship = this.props.ship; const ews = ship.internal.filter(o => o.m && o.m.grp === 'ews'); let expCap; if(ews.length < 1){ expCap = 4; } else{ expCap = ews[0].m.class == 3 ? 5 : 6; } return expCap <= this.props.ship.hardpoints.filter(o => o.m && o.m.experimental).length; } /** * Generate React Components for Module Group * @param {Ship} ship Ship the selection is for * @param {Function} translate Translate function * @param {Object} mountedModule Mounted Module * @param {Function} warningFunc Warning function * @param {function} onSelect Select/Mount callback * @param {string} grp Group name * @param {Array} modules Available modules * @return {React.Component} Available Module Group contents */ _buildGroup(ship, translate, mountedModule, warningFunc, onSelect, grp, modules) { 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]; // If m.grp is mh or mm, or m.symbol contains 'Missing' skip it if (m.grp == 'mh' || m.grp == 'mm' || (typeof(m.symbol) !== 'undefined' && m.symbol.includes("Missing"))) { // If this is a missing module, skip it continue; } let mount = null; let disabled = false; prevName = m.name; if (ModuleUtils.isShieldGenerator(m.grp)) { // Shield generators care about maximum hull mass disabled = ship.hullMass > m.maxmass; // If the mounted module is experimental as well, we can replace it so // the maximum does not apply } else if (m.experimental && (!mountedModule || !mountedModule.experimental)) { disabled = this._experimentalCapacityReached(); } else if (m.grp === 'mlc' && (!mountedModule || mountedModule.grp !== 'mlc')) { disabled = 1 <= ship.internal.filter(o => o.m && o.m.grp === 'mlc').length; } 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