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, MatIcon } 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'; import ModalShoppingList from '../components/ModalShoppingList'; /** * 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); this._sectionMenuRefs = {}; } /** * [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(',')); } /** * Generates the shopping list */ _genShoppingList() { this.context.showModal(); } /** * 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) { if (textKey.startsWith('empire') || textKey.startsWith('federation')) { requirementElements.push(
{translate(textKey)}
); } else { requirementElements.push(
{translate(textKey)}
); } } 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}
{/* Main tables */} {/* Control of ship and opponent */}

{translate('ship control')}

{ ship.cargoCapacity > 0 ? : null }

{translate('opponent')}

{/* Tabbed subpages */}
); } }