diff --git a/package.json b/package.json index aaf8d19e..5ce7ae59 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "fbemitter": "^2.0.0", "lodash": "^4.15.0", "lz-string": "^1.4.4", + "react-numeric-input": "^2.0.6", "react": "^15.0.1", "react-dom": "^15.0.1", "superagent": "^1.4.0" diff --git a/src/app/components/ModSlider.jsx b/src/app/components/ModSlider.jsx new file mode 100644 index 00000000..057742c4 --- /dev/null +++ b/src/app/components/ModSlider.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import Slider from './Slider'; + +const MARGIN_LR = 8; // Left/ Right margin + +/** + * Horizontal Slider for modifications + */ +export default class ModSlider extends Slider { + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render the slider + * @return {React.Component} The slider + */ + render() { + let outerWidth = this.state.outerWidth; + let { axis, axisUnit, min, max, scale } = this.props; + + let style = { + width: '100%', + height: axis ? '2.5em' : '1.5em', + boxSizing: 'border-box' + }; + + if (!outerWidth) { + return ; + } + + let margin = MARGIN_LR * scale; + let width = outerWidth - (margin * 2); + let pctPos = width * this.props.percent + margin; + + // TODO add this back in from middle to point + // + return + + + + {axis && + {min + axisUnit} + {(min + max / 2) + axisUnit} + {max + axisUnit} + } + ; + } +} diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx new file mode 100644 index 00000000..c66df4e1 --- /dev/null +++ b/src/app/components/ModificationsMenu.jsx @@ -0,0 +1,208 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import NumericInput from 'react-numeric-input'; +import TranslatedComponent from './TranslatedComponent'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import cn from 'classnames'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; +import { Modifications } from 'coriolis-data/dist'; +import ModSlider from './ModSlider'; + +const PRESS_THRESHOLD = 500; // mouse/touch down threshold + +/** + * Modifications menu + */ +export default class ModificationsMenu extends TranslatedComponent { + + static propTypes = { + ship: React.PropTypes.object.isRequired, + m: React.PropTypes.object.isRequired, + onChange: React.PropTypes.func.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + this.state = this._initState(props, context); + } + + /** + * Initiate the list of modifications + * @param {Object} props React Component properties + * @param {Object} context React Component context + * @return {Object} list: Array of React Components + */ + _initState(props, context) { + let translate = context.language.translate; + let formats = context.language.formats; + let { m } = props; + let list = []; + + for (let modId of Modifications.validity[m.grp]) { + list.push(
+
{translate(Modifications.modifiers[modId].name)}
+ {formats.pct(m.getModValue(modId) || 0)} + +
); + } + // + + return { list }; + } + + /** + * Generate React Components for Module Group + * @param {Function} translate Translate function + * @param {Objecy} mountedModule Mounted Module + * @param {Funciton} warningFunc Warning function + * @param {number} mass Mass + * @param {function} onSelect Select/Mount callback + * @param {string} grp Group name + * @param {Array} modules Available modules + * @return {React.Component} Available Module Group contents + */ + _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) { + let prevClass = null, prevRating = null; + let elems = []; + + for (let i = 0; i < modules.length; i++) { + let m = modules[i]; + let mount = null; + let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; + let active = mountedModule && mountedModule === m; + let classes = cn(m.name ? 'lc' : 'c', { + warning: !disabled && warningFunc && warningFunc(m), + active, + disabled + }); + let eventHandlers; + + if (disabled || active) { + eventHandlers = {}; + } else { + 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 + }; + } + + switch(m.mount) { + case 'F': mount = ; break; + case 'G': mount = ; break; + case 'T': mount = ; break; + } + + if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') { + elems.push(
); + } + + elems.push( +
  • + {mount} + {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')} +
  • + ); + prevClass = m.class; + prevRating = m.rating; + } + + return
      {elems}
    ; + } + + + /** + * Touch 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(); + } + } + + /** + * Scroll to mounted (if it exists) module group on mount + */ + componentDidMount() { + if (this.groupElem) { // Scroll to currently selected group + findDOMNode(this).scrollTop = this.groupElem.offsetTop; + } + } + + /** + * 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)); + } + + + /** + * Update modification given a value. + * @param {Number} modId The ID of the modification + * @param {Number} value The value to set, in the range [0,1] + */ + _updateValue(modId, value) { + let scaledValue = (value - 0.5) * 2; + let m = this.props.m; + let ship = this.props.ship; + ship.setModification(m, modId, scaledValue); + this.props.onChange(); + } + + /** + * Obtain slider value from a modification. + * @param {Number} modId The ID of the modification + * @return {Number} value The value of the slider, in the range [0,1] + */ + _getSliderPercent(modId) { + let m = this.props.m; + if (m.getModValue(modId)) { + return (m.getModValue(modId) / 2) + 0.5; + } + return 0.5; + } + + /** + * Render the list + * @return {React.Component} List + */ + render() { + return ( +
    e.stopPropagation() } + onContextMenu={stopCtxPropagation} + > + {this.state.list} +
    + ); + } + +} diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index 7ebc8163..84aee6b8 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -92,8 +92,8 @@ export default class ShipSummaryTable extends TranslatedComponent { {sgRecover} {sgRecharge} {ship.hullMass} {u.T} - {round(ship.unladenMass)} {u.T} - {round(ship.ladenMass)} {u.T} + {int(ship.unladenMass)} {u.T} + {int(ship.ladenMass)} {u.T} {round(ship.cargoCapacity)} {u.T} {round(ship.fuelCapacity)} {u.T} {round(ship.unladenRange)} {u.LY} diff --git a/src/app/components/Slot.jsx b/src/app/components/Slot.jsx index f0d64ccb..0453e23a 100644 --- a/src/app/components/Slot.jsx +++ b/src/app/components/Slot.jsx @@ -2,6 +2,7 @@ import React from 'react'; 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'; @@ -94,6 +95,12 @@ export default class Slot extends TranslatedComponent { />; } + if (this.props.selected) { + menu = ; + } // TODO: implement touch dragging return ( diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 55d554fb..1d15d6f4 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -4,8 +4,8 @@ import TranslatedComponent from './TranslatedComponent'; import { jumpRange } from '../shipyard/Calculations'; import { diffDetails } from '../utils/SlotFunctions'; import AvailableModulesMenu from './AvailableModulesMenu'; +import ModificationsMenu from './ModificationsMenu'; import { ListModifications } from './SvgIcons'; -import Slider from './Slider'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -32,7 +32,7 @@ export default class StandardSlot extends TranslatedComponent { render() { let { termtip, tooltip } = this.context; let { translate, formats, units } = this.context.language; - let { modules, slot, warning, onSelect, ladenMass, ship } = this.props; + let { modules, slot, warning, onSelect, onChange, ladenMass, ship } = this.props; let m = slot.m; let classRating = m.class + m.rating; let menu; @@ -50,13 +50,22 @@ export default class StandardSlot extends TranslatedComponent { />; } + if (this.props.selected) { + menu = ; + } + return (
    {slot.maxClass}
    {classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}
    -
    {m.getMass() || m.fuel || 0}{units.T}
    +
    {formats.round1(m.getMass()) || m.fuel || 0}{units.T}
    { m.grp == 'bh' && m.name ?
    {translate(m.name)}
    : null } @@ -70,46 +79,12 @@ export default class StandardSlot extends TranslatedComponent { { m.weaponcapacity ?
    {translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}
    : null } { m.systemcapacity ?
    {translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}
    : null } { m.enginecapacity ?
    {translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}
    : null } - { validMods.length > 0 ?
    : null }
    {menu}
    ); + //{ validMods.length > 0 ?
    : null } } - // {validMods.length > 0 ?
    : null } - - _showModificationsMenu(m, e) { - let validMods = m == null ? [] : (Modifications.validity[m.grp] || []); - // TODO set up the modifications - e.stopPropagation(); - } - - /** - * Update power usage modification given a slider value. - * Note that this is a temporary function until we have a slider section - * @param {Number} value The value of the slider - */ - _updateSliderValue(value) { - let m = this.props.slot.m; - if (m) { - m.setModValue(2, value * 2 - 1); - } - this.props.onChange(); - } - - /** - * Obtain slider value from a power usage modification. - * Note that this is a temporary function until we have a slider section - * @return {Number} value The value of the slider - */ - _getSliderValue() { - let m = this.props.slot.m; - if (m && m.getModValue(2)) { - return (m.getModValue(2) + 1) / 2; - } - return 0; - } - } diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index e4ee5633..839c6b00 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -284,9 +284,9 @@ export default class OutfittingPage extends Page { canSave = (newBuildName || buildName) && code !== savedCode, canRename = buildName && newBuildName && buildName != newBuildName, canReload = savedCode && canSave, - hStr = ship.getHardpointsString(), - sStr = ship.getStandardString(), - iStr = ship.getInternalString(); + hStr = ship.getHardpointsString() + '.' + ship.getModificationsString(), + sStr = ship.getStandardString() + '.' + ship.getModificationsString(), + iStr = ship.getInternalString() + '.' + ship.getModificationsString(); return (
    diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index d3377e6b..3af7768a 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -82,13 +82,13 @@ export default class Module { } /** - * Get the mass of this module, taking in to account modifications - * @return {Number} the mass of this module + * Get the integrity of this module, taking in to account modifications + * @return {Number} the integrity of this module */ - getMass() { + getIntegrity() { let result = 0; - if (this.mass) { - result = this.mass; + if (this.health) { + result = this.health; if (result) { let mult = this.getModValue(3); if (mult) { result = result * (1 + mult); } @@ -98,13 +98,13 @@ export default class Module { } /** - * Get the integrity of this module, taking in to account modifications - * @return {Number} the integrity of this module + * Get the mass of this module, taking in to account modifications + * @return {Number} the mass of this module */ - getIntegrity() { + getMass() { let result = 0; - if (this.health) { - result = this.health; + if (this.mass) { + result = this.mass; if (result) { let mult = this.getModValue(4); if (mult) { result = result * (1 + mult); } diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 5123f087..50d4018f 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -347,7 +347,6 @@ export default class Ship { return this.serialized.hardpoints; } - /** * Serializes the modifications to a string * @return {String} Serialized modifications 'code' @@ -403,6 +402,37 @@ export default class Ship { return this; } + /** + * Set a modification value + * @param {Object} m The module to change + * @param {Object} mId The ID of the modification to change + * @param {Number} value The new value of the modification + */ + setModification(m, mId, value) { + // Handle special cases + if (mId == 1) { + // Power generation + m.setModValue(mId, value); + this.updatePower(); + } else if (mId == 2) { + // Power usage + m.setModValue(mId, value); + this.updatePower(); + } else if (mId == 4) { + // Mass + let oldMass = m.getMass(); + m.setModValue(mId, value); + let newMass = m.getMass(); + this.unladenMass = this.unladenMass - oldMass + newMass; + this.ladenMass = this.ladenMass - oldMass + newMass; + this.updateTopSpeed(); + this.updateJumpStats(); + } else { + // Generic + m.setModValue(mId, value); + } + } + /** * Builds/Updates the ship instance with the ModuleUtils[comps] passed in. * @param {Object} comps Collection of ModuleUtils used to build the ship diff --git a/src/less/app.less b/src/less/app.less index b4a5443f..7f4564ec 100755 --- a/src/less/app.less +++ b/src/less/app.less @@ -163,4 +163,4 @@ footer { float: right; text-align: right; } -} \ No newline at end of file +}