mirror of
https://github.com/EDCD/coriolis.git
synced 2025-12-09 06:43:24 +00:00
Updates for mods UI
This commit is contained in:
@@ -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"
|
||||
|
||||
55
src/app/components/ModSlider.jsx
Normal file
55
src/app/components/ModSlider.jsx
Normal 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>;
|
||||
}
|
||||
}
|
||||
208
src/app/components/ModificationsMenu.jsx
Normal file
208
src/app/components/ModificationsMenu.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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' }}>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -163,4 +163,4 @@ footer {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user