Updates for mods UI

This commit is contained in:
Cmdr McDonald
2016-10-27 13:26:09 +01:00
parent 45337913ba
commit 3114852c63
10 changed files with 331 additions and 55 deletions

View File

@@ -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"

View File

@@ -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 <svg style={style} />;
}
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
// <rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
return <svg onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onTouchEnd={this._up} style={style}>
<rect className='primary' style={{ opacity: 0.3, fillOpacity: 0 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
<circle className='primary' r={margin} cy='0.6em' style={{ strokeWidth: 0, fillOpacity: 1 }} cx={pctPos} />
<rect x={margin} width={width} height='100%' fillOpacity='0' style={{ cursor: 'col-resize' }} onMouseDown={this._down} onTouchMove={this._move} onTouchStart={this._down} />
{axis && <g style={{ fontSize: '.7em' }}>
<text className='primary-disabled' y='3em' x={margin} style={{ textAnchor: 'middle' }}>{min + axisUnit}</text>
<text className='primary-disabled' y='3em' x='50%' style={{ textAnchor: 'middle' }}>{(min + max / 2) + axisUnit}</text>
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
</g>}
</svg>;
}
}

View File

@@ -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(<div className={'cb'} key={modId}>
<div className={'l'}>{translate(Modifications.modifiers[modId].name)}</div>
<span className={'r'}>{formats.pct(m.getModValue(modId) || 0)}</span>
<ModSlider className={'cb'} percent={this._getSliderPercent(modId)} onChange={this._updateValue.bind(this, modId)} />
</div>);
}
//<NumericInput className={'r'} min={-100} max={100} step={0.1} precision={2} value={m.getModValue(modId) * 100} onChange={this._updateValue.bind(this, modId)} />
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 = <MountFixed className={'lg'} />; break;
case 'G': mount = <MountGimballed className={'lg'}/>; break;
case 'T': mount = <MountTurret className={'lg'}/>; break;
}
if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') {
elems.push(<br key={'b' + m.grp + i} />);
}
elems.push(
<li key={m.id} className={classes} {...eventHandlers}>
{mount}
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
</li>
);
prevClass = m.class;
prevRating = m.rating;
}
return <ul key={'modules' + grp} >{elems}</ul>;
}
/**
* 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 (
<div
className={cn('select', this.props.className)}
onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation}
>
{this.state.list}
</div>
);
}
}

View File

@@ -92,8 +92,8 @@ export default class ShipSummaryTable extends TranslatedComponent {
<td className={sgClassNames}>{sgRecover}</td>
<td className={sgClassNames}>{sgRecharge}</td>
<td>{ship.hullMass} {u.T}</td>
<td>{round(ship.unladenMass)} {u.T}</td>
<td>{round(ship.ladenMass)} {u.T}</td>
<td>{int(ship.unladenMass)} {u.T}</td>
<td>{int(ship.ladenMass)} {u.T}</td>
<td>{round(ship.cargoCapacity)} {u.T}</td>
<td>{round(ship.fuelCapacity)} {u.T}</td>
<td>{round(ship.unladenRange)} {u.LY}</td>

View File

@@ -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 = <ModificationsMenu
className={this._getClassNames()}
m={m}
/>;
}
// TODO: implement touch dragging
return (

View File

@@ -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 = <ModificationsMenu
className='standard'
onChange={onChange}
ship={ship}
m={m}
/>;
}
return (
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen} onContextMenu={stopCtxPropagation}>
<div className={cn('details-container', { warning: warning && warning(slot.m) })}>
<div className={'sz'}>{slot.maxClass}</div>
<div>
<div className='l'>{classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}</div>
<div className={'r'}>{m.getMass() || m.fuel || 0}{units.T}</div>
<div className={'r'}>{formats.round1(m.getMass()) || m.fuel || 0}{units.T}</div>
<div/>
<div className={'cb'}>
{ m.grp == 'bh' && m.name ? <div className='l'>{translate(m.name)}</div> : null }
@@ -70,46 +79,12 @@ export default class StandardSlot extends TranslatedComponent {
{ m.weaponcapacity ? <div className='l'>{translate('WEP')}: {m.weaponcapacity}{units.MJ} / {m.weaponrecharge}{units.MW}</div> : null }
{ m.systemcapacity ? <div className='l'>{translate('SYS')}: {m.systemcapacity}{units.MJ} / {m.systemrecharge}{units.MW}</div> : null }
{ m.enginecapacity ? <div className='l'>{translate('ENG')}: {m.enginecapacity}{units.MJ} / {m.enginerecharge}{units.MW}</div> : null }
{ validMods.length > 0 ? <div className='r' ><button onClick={this._showModificationsMenu.bind(this, m)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>
</div>
{menu}
</div>
);
//{ validMods.length > 0 ? <div className='r' ><button onClick={this._showModificationsMenu.bind(this, m)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
}
// {validMods.length > 0 ? <div className='cb' ><Slider onChange={this._updateSliderValue.bind(this)} min={-1} max={1} percent={this._getSliderValue()}/></div> : 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;
}
}

View File

@@ -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 (
<div id='outfit' className={'page'} style={{ fontSize: (sizeRatio * 0.9) + 'em' }}>

View File

@@ -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); }

View File

@@ -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

View File

@@ -163,4 +163,4 @@ footer {
float: right;
text-align: right;
}
}
}