Compare commits

...

4 Commits

Author SHA1 Message Date
Felix Linker
2eed1bc85b Add time to deplete armour/shields 2021-05-15 20:21:00 +02:00
Felix Linker
3469af10b6 Update ShipPicker 2021-05-15 15:23:36 +02:00
Felix Linker
629ba35bc5 Update engagement range and slider 2021-05-15 14:58:10 +02:00
Felix Linker
e453ff73b7 Migrate changes to slot-representation 2021-05-14 10:22:28 +02:00
5 changed files with 160 additions and 340 deletions

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import Slider from '../components/Slider'; import Slider from '../components/Slider';
import { moduleReduce } from 'ed-forge/lib/helper';
/** /**
* Engagement range slider * Engagement range slider
@@ -21,35 +22,18 @@ export default class EngagementRange extends TranslatedComponent {
*/ */
constructor(props, context) { constructor(props, context) {
super(props); super(props);
const { ship } = props;
const maxRange = Math.round(this._calcMaxRange(ship));
this.state = { this.state = {
maxRange maxRange: moduleReduce(
this.props.ship.getHardpoints(),
'maximumrange',
true,
// Don't use plain `Math.max` because callback will be passed four args
(a, v) => Math.max(a, v),
1000,
),
}; };
} }
/**
* Calculate the maximum range of a ship's weapons
* @param {Object} ship The ship
* @returns {int} The maximum range, in metres
*/
_calcMaxRange(ship) {
let maxRange = 1000;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const thisRange = ship.hardpoints[i].m.getRange();
if (thisRange > maxRange) {
maxRange = thisRange;
}
}
}
return maxRange;
}
/** /**
* Update range * Update range
* @param {number} rangeLevel percentage level from 0 to 1 * @param {number} rangeLevel percentage level from 0 to 1
@@ -61,7 +45,9 @@ export default class EngagementRange extends TranslatedComponent {
const range = Math.round(rangeLevel * maxRange); const range = Math.round(rangeLevel * maxRange);
if (range !== this.props.engagementRange) { if (range !== this.props.engagementRange) {
this.props.onChange(range); const { onChange, ship } = this.props;
ship.setEngagementRange(range);
onChange(range);
} }
} }
@@ -70,8 +56,8 @@ export default class EngagementRange extends TranslatedComponent {
* @return {React.Component} contents * @return {React.Component} contents
*/ */
render() { render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { language, onWindowResize, sizeRatio } = this.context;
const { formats, translate, units } = language; const { formats, translate } = language;
const { engagementRange } = this.props; const { engagementRange } = this.props;
const { maxRange } = this.state; const { maxRange } = this.state;

View File

@@ -82,12 +82,14 @@ export default class Offence extends TranslatedComponent {
const { formats, translate, units } = language; const { formats, translate, units } = language;
const sortOrder = this._sortOrder; const sortOrder = this._sortOrder;
const damage = ship.getMetrics(DAMAGE_METRICS); const {
drained, sustained, rangeMultiplier, hardnessMultiplier, timeToDrain
} = ship.getMetrics(DAMAGE_METRICS);
const portions = { const portions = {
Absolute: damage.types.abs, Absolute: sustained.types.abs,
Explosive: damage.types.expl, Explosive: sustained.types.expl,
Kinetic: damage.types.kin, Kinetic: sustained.types.kin,
Thermic: damage.types.therm, Thermic: sustained.types.therm,
}; };
const oppShield = ship.getOpponent().getShield(); const oppShield = ship.getOpponent().getShield();
@@ -106,8 +108,8 @@ export default class Offence extends TranslatedComponent {
Thermic: oppArmour.thermal.damageMultiplier, Thermic: oppArmour.thermal.damageMultiplier,
}; };
let rows = []; const weapons = sortBy(ship.getHardpoints(), (m) => m.get('distributordraw'));
for (let weapon of ship.getHardpoints()) { let rows = weapons.map((weapon) => {
const sdps = weapon.get('sustaineddamagepersecond'); const sdps = weapon.get('sustaineddamagepersecond');
const byRange = weapon.getRangeEffectiveness(); const byRange = weapon.getRangeEffectiveness();
const weaponPortions = { const weaponPortions = {
@@ -176,7 +178,7 @@ export default class Offence extends TranslatedComponent {
if (exp) { if (exp) {
bpTitle += `, ${translate(exp)}`; bpTitle += `, ${translate(exp)}`;
} }
rows.push({ return {
slot: weapon.getSlot(), slot: weapon.getSlot(),
mount: weapon.mount, mount: weapon.mount,
classRating: weapon.getClassRating(), classRating: weapon.getClassRating(),
@@ -192,8 +194,9 @@ export default class Offence extends TranslatedComponent {
armourEft, armourEft,
armourSdpsTooltip, armourSdpsTooltip,
armourEftTooltip, armourEftTooltip,
}); };
} });
const { predicate, desc } = this.state; const { predicate, desc } = this.state;
rows = sortBy(rows, (row) => row[predicate]); rows = sortBy(rows, (row) => row[predicate]);
if (desc) { if (desc) {
@@ -202,18 +205,18 @@ export default class Offence extends TranslatedComponent {
const sdpsTooltip = objToTooltip( const sdpsTooltip = objToTooltip(
translate, translate,
mapValues(portions, (p) => formats.f1(damage.sustained.dps * p)), mapValues(portions, (p) => formats.f1(sustained.dps * p)),
); );
const sdpsPie = objToPie( const sdpsPie = objToPie(
translate, translate,
mapValues(portions, (p) => Math.round(damage.sustained.dps * p)), mapValues(portions, (p) => Math.round(sustained.dps * p)),
); );
const shieldSdpsSrcs = mergeWith( const shieldSdpsSrcs = mergeWith(
clone(portions), clone(portions),
shieldMults, shieldMults,
(objV, srcV) => damage.sustained.dps * oppShield.absolute.bySys * (objV, srcV) => sustained.dps * oppShield.absolute.bySys *
damage.rangeMultiplier * objV * srcV, rangeMultiplier * objV * srcV,
); );
const shieldsSdps = sum(values(shieldSdpsSrcs)); const shieldsSdps = sum(values(shieldSdpsSrcs));
const shieldsSdpsTooltip = objToTooltip( const shieldsSdpsTooltip = objToTooltip(
@@ -228,8 +231,8 @@ export default class Offence extends TranslatedComponent {
const armourSdpsSrcs = mergeWith( const armourSdpsSrcs = mergeWith(
clone(portions), clone(portions),
armourMults, armourMults,
(objV, srcV) => damage.sustained.dps * damage.hardnessMultiplier * (objV, srcV) => sustained.dps * hardnessMultiplier * rangeMultiplier *
damage.rangeMultiplier * objV * srcV, objV * srcV,
); );
const armourSdps = sum(values(armourSdpsSrcs)); const armourSdps = sum(values(armourSdpsSrcs));
const totalArmourSDpsTooltipDetails = objToTooltip( const totalArmourSDpsTooltipDetails = objToTooltip(
@@ -241,10 +244,45 @@ export default class Offence extends TranslatedComponent {
mapValues(armourSdpsSrcs, (v) => Math.round(v)), mapValues(armourSdpsSrcs, (v) => Math.round(v)),
); );
const pd = ship.getPowerDistributor(); const drainedPortions = {
const timeToDrain = damage.sustained.timeToDrain[ship.getDistributorSettings().Wep]; Absolute: drained.types.abs,
// const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, shieldsSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); Explosive: drained.types.expl,
// const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, armourSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4)); Kinetic: drained.types.kin,
Thermic: drained.types.therm,
};
// How much damage do we deal, before the capacitor is empty?
const armourLeft = oppArmour.armour - (timeToDrain * armourSdps);
// If we can't kill the enemy on one capacitor, factor in drained damage
let timeToDepleteArmour;
if (armourLeft > 0) {
const effectiveDrainedDps = sum(values(mergeWith(
clone(drainedPortions),
armourMults,
(objV, srcV) => objV * srcV,
))) * drained.dps * rangeMultiplier *
hardnessMultiplier;
timeToDepleteArmour = effectiveDrainedDps === 0 ? Infinity :
timeToDrain + (armourLeft / effectiveDrainedDps);
} else {
timeToDepleteArmour = oppArmour.armour / armourSdps;
}
// How much damage do we deal, before the capacitor is empty?
const shieldsLeft = oppShield.withSCBs - (timeToDrain * shieldsSdps);
// If we can't kill the enemy on one capacitor, factor in drained damage
let timeToDepleteShields;
if (shieldsLeft > 0) {
const effectiveDrainedDps = sum(values(mergeWith(
clone(drainedPortions),
shieldMults,
(objV, srcV) => objV * srcV,
))) * drained.dps * rangeMultiplier;
timeToDepleteShields = effectiveDrainedDps === 0 ? Infinity :
timeToDrain + (shieldsLeft / effectiveDrainedDps);
} else {
timeToDepleteShields = oppShield.withSCBs / shieldsSdps;
}
return ( return (
<span id='offence'> <span id='offence'>
@@ -307,7 +345,7 @@ export default class Offence extends TranslatedComponent {
<td></td> <td></td>
<td className='ri'> <td className='ri'>
<span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}> <span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
={formats.f1(damage.sustained.dps)} ={formats.f1(sustained.dps)}
</span> </span>
</td> </td>
<td className='ri'> <td className='ri'>
@@ -342,8 +380,7 @@ export default class Offence extends TranslatedComponent {
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))} <h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))}
onMouseOut={tooltip.bind(null, null)}> onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/> {translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>
ToDo {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}
{/* {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)} */}
</h2> </h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))} <h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))}
onMouseOut={tooltip.bind(null, null)}> onMouseOut={tooltip.bind(null, null)}>
@@ -353,8 +390,7 @@ export default class Offence extends TranslatedComponent {
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))} <h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))}
onMouseOut={tooltip.bind(null, null)}> onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/> {translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>
ToDo {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}
{/* {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)} */}
</h2> </h2>
</div> </div>
<div className='group quarter'> <div className='group quarter'>

View File

@@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent'; import TranslatedComponent from './TranslatedComponent';
import { Ships } from 'coriolis-data/dist';
import { Rocket } from './SvgIcons'; import { Rocket } from './SvgIcons';
import Persist from '../stores/Persist'; import Persist from '../stores/Persist';
import cn from 'classnames'; import cn from 'classnames';
import { Factory, Ship } from 'ed-forge';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { isEqual } from 'lodash';
/** /**
* Ship picker * Ship picker
@@ -14,14 +15,9 @@ import autoBind from 'auto-bind';
export default class ShipPicker extends TranslatedComponent { export default class ShipPicker extends TranslatedComponent {
static propTypes = { static propTypes = {
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
ship: PropTypes.string.isRequired, ship: PropTypes.instanceOf(Ship).isRequired,
build: PropTypes.string
}; };
static defaultProps = {
ship: 'eagle'
}
/** /**
* constructor * constructor
* @param {object} props Properties react * @param {object} props Properties react
@@ -30,20 +26,38 @@ export default class ShipPicker extends TranslatedComponent {
constructor(props, context) { constructor(props, context) {
super(props); super(props);
autoBind(this); autoBind(this);
this.state = { menuOpen: false }; this.state = {
menuOpen: false,
opponent: {
self: true,
type: props.ship.getShipType(),
stock: false,
id: undefined,
},
};
} }
/** /**
* Update ship * Update ship
* @param {object} ship the ship * @param {boolean} self True to compare with ship itself
* @param {string} build the build, if present * @param {object} type The ship type
* @param {boolean} stock True to compare with a stock version of given type
* @param {string} id The build's stored ID
*/ */
_shipChange(ship, build) { _shipChange(self, type, stock = false, id = null) {
this._closeMenu(); const opponent = { self, type, stock, id };
if (isEqual(opponent, this.state.opponent)) {
// Ensure that the ship has changed this.setState({ menuOpen: false });
if (ship !== this.props.ship || build !== this.props.build) { } else {
this.props.onChange(ship, build); const { onChange } = this.props;
if (self) {
onChange(this.props.ship);
} else if (stock) {
onChange(Factory.newShip(type));
} else {
onChange(new Ship(Persist.getBuild(type, id)));
}
this.setState({ menuOpen: false, opponent });
} }
} }
@@ -52,26 +66,41 @@ export default class ShipPicker extends TranslatedComponent {
* @returns {object} the picker menu * @returns {object} the picker menu
*/ */
_renderPickerMenu() { _renderPickerMenu() {
const { ship, build } = this.props; const { menuOpen } = this.state;
const _shipChange = this._shipChange; if (!menuOpen) {
const builds = Persist.getBuilds(); return null;
const buildList = [];
for (let shipId of this.shipOrder) {
const shipBuilds = [];
// Add stock build
const stockSelected = (ship == shipId && !build);
shipBuilds.push(<li key={shipId} className={ cn({ 'selected': stockSelected })} onClick={_shipChange.bind(this, shipId, null)}>Stock</li>);
if (builds[shipId]) {
let buildNameOrder = Object.keys(builds[shipId]).sort();
for (let buildName of buildNameOrder) {
const buildSelected = ship === shipId && build === buildName;
shipBuilds.push(<li key={shipId + '-' + buildName} className={ cn({ 'selected': buildSelected })} onClick={_shipChange.bind(this, shipId, buildName)}>{buildName}</li>);
}
}
buildList.push(<ul key={shipId} className='block'>{Ships[shipId].properties.name}{shipBuilds}</ul>);
} }
return buildList; const { translate } = this.context.language;
const { self, type, stock, id } = this.state.opponent;
return <div className='menu-list' onClick={(e) => e.stopPropagation()}>
<div className='quad'>
{Factory.getAllShipTypes().sort().map((shipType) =>
<ul key={shipType} className='block'>
{translate(shipType)}
{/* Add stock build */}
<li key={shipType}
onClick={this._shipChange.bind(this, false, shipType, true)}
className={cn({ selected: stock && type === shipType })}>
{translate('stock')}
</li>
{Persist.getBuildsNamesFor(shipType).sort().map((storedId) =>
<li key={`${shipType}-${storedId}`}
onClick={this._shipChange.bind(this, false, shipType, false, storedId)}
className={ cn({ selected: type === shipType && id === storedId })}>
{storedId}
</li>)}
{/* Add ship itself */}
{(this.props.ship.getShipType() === shipType ?
<li key='self'
onClick={this._shipChange.bind(this, true, shipType)}
className={cn({ selected: self })}>
{translate('THIS_SHIP')}
</li> :
null)}
</ul>)}
</div>
</div>;
} }
/** /**
@@ -82,40 +111,35 @@ export default class ShipPicker extends TranslatedComponent {
this.setState({ menuOpen: !menuOpen }); this.setState({ menuOpen: !menuOpen });
} }
/**
* Close the menu
*/
_closeMenu() {
const { menuOpen } = this.state;
if (menuOpen) {
this._toggleMenu();
}
}
/** /**
* Render picker * Render picker
* @return {React.Component} contents * @return {React.Component} contents
*/ */
render() { render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context; const { translate } = this.context.language;
const { formats, translate, units } = language; const { ship } = this.props;
const { ship, build } = this.props;
const { menuOpen } = this.state; const { menuOpen } = this.state;
const { self, type, stock, id } = this.state.opponent;
let label;
if (self) {
label = translate('THIS_SHIP');
} else if (stock) {
label = translate('stock');
} else {
label = id;
}
const shipString = ship + ': ' + (build ? build : translate('stock'));
return ( return (
<div className='shippicker' onClick={ (e) => e.stopPropagation() }> <div className='shippicker' onClick={ (e) => e.stopPropagation() }>
<div className='menu'> <div className='menu'>
<div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}> <div className={cn('menu-header', { selected: menuOpen })} onClick={this._toggleMenu}>
<span><Rocket className='warning' /></span> <span><Rocket className='warning' /></span>
<span className='menu-item-label'>{shipString}</span> <span className='menu-item-label'>
{`${translate(type)}: ${label}`}
</span>
</div> </div>
{ menuOpen ? {this._renderPickerMenu()}
<div className='menu-list' onClick={ (e) => e.stopPropagation() }>
<div className='quad'>
{this._renderPickerMenu()}
</div>
</div> : null }
</div> </div>
</div> </div>
); );

View File

@@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import autoBind from 'auto-bind';
const MARGIN_LR = 8; // Left/ Right margin const MARGIN_LR = 8; // Left/ Right margin
@@ -7,7 +8,6 @@ const MARGIN_LR = 8; // Left/ Right margin
* Horizontal Slider * Horizontal Slider
*/ */
export default class Slider extends React.Component { export default class Slider extends React.Component {
static defaultProps = { static defaultProps = {
axis: false, axis: false,
min: 0, min: 0,
@@ -32,16 +32,7 @@ export default class Slider extends React.Component {
*/ */
constructor(props) { constructor(props) {
super(props); super(props);
this._down = this._down.bind(this); autoBind(this);
this._move = this._move.bind(this);
this._up = this._up.bind(this);
this._keyup = this._keyup.bind(this);
this._keydown = this._keydown.bind(this);
this._touchstart = this._touchstart.bind(this);
this._touchend = this._touchend.bind(this);
this._updatePercent = this._updatePercent.bind(this);
this._updateDimensions = this._updateDimensions.bind(this);
this.state = { width: 0 }; this.state = { width: 0 };
} }
@@ -55,7 +46,6 @@ export default class Slider extends React.Component {
this.left = rect.left; this.left = rect.left;
this.width = rect.width; this.width = rect.width;
this._move(event); this._move(event);
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
} }
/** /**
@@ -75,70 +65,11 @@ export default class Slider extends React.Component {
* @param {Event} event DOM Event * @param {Event} event DOM Event
*/ */
_up(event) { _up(event) {
this.sliderInputBox.sliderVal.focus();
clearTimeout(this.touchStartTimer);
event.preventDefault(); event.preventDefault();
this.left = null; this.left = null;
this.width = null; this.width = null;
} }
/**
* Key up handler for keyboard.
* display the number field then set focus to it
* when "Enter" key is pressed
* @param {Event} event Keyboard event
*/
_keyup(event) {
switch (event.key) {
case 'Enter':
event.preventDefault();
this.sliderInputBox._setDisplay('block');
return;
default:
return;
}
}
/**
* Key down handler
* increment slider position by +/- 1 when right/left arrow key is pressed or held
* @param {Event} event Keyboard even
*/
_keydown(event) {
let newVal = this.props.percent * this.props.max;
switch (event.key) {
case 'ArrowRight':
newVal += 1;
if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max);
return;
case 'ArrowLeft':
newVal -= 1;
if (newVal >= 0) this.props.onChange(newVal / this.props.max);
return;
default:
return;
}
}
/**
* Touch start handler
* @param {Event} event DOM Event
*
*/
_touchstart(event) {
this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500);
}
/**
* Touch end handler
* @param {Event} event DOM Event
*
*/
_touchend(event) {
this.sliderInputBox.sliderVal.focus();
clearTimeout(this.touchStartTimer);
}
/** /**
* Determine if the user is still dragging * Determine if the user is still dragging
* @param {SyntheticEvent} event Event * @param {SyntheticEvent} event Event
@@ -213,7 +144,7 @@ export default class Slider extends React.Component {
let width = outerWidth - (margin * 2); let width = outerWidth - (margin * 2);
let pctPos = width * this.props.percent; let pctPos = width * this.props.percent;
return <div><svg return <div><svg
onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} onKeyUp={this._keyup} onKeyDown={this._keydown} style={style} ref={node => this.node = node} tabIndex="0"> onMouseUp={this._up} onMouseEnter={this._enter.bind(this)} onMouseMove={this._move} style={style} ref={node => this.node = node} tabIndex="0">
<rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' /> <rect className='primary' style={{ opacity: 0.3 }} x={margin} y='0.25em' rx='0.3em' ry='0.3em' width={width} height='0.7em' />
<rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' /> <rect className='primary-disabled' x={margin} y='0.45em' rx='0.15em' ry='0.15em' width={pctPos} height='0.3em' />
<circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} /> <circle className='primary' r={margin} cy='0.6em' cx={pctPos + margin} />
@@ -224,163 +155,6 @@ export default class Slider extends React.Component {
<text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text> <text className='primary-disabled' y='3em' x='100%' style={{ textAnchor: 'end' }}>{max + axisUnit}</text>
</g>} </g>}
</svg> </svg>
<TextInputBox ref={(tb) => this.sliderInputBox = tb}
onChange={this.props.onChange}
percent={this.props.percent}
axisUnit={this.props.axisUnit}
scale={this.props.scale}
max={this.props.max}
/>
</div>; </div>;
} }
} }
/**
* New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android)
**/
class TextInputBox extends React.Component {
static propTypes = {
axisUnit: PropTypes.string,// units (T, M, etc.)
max: PropTypes.number,
onChange: PropTypes.func.isRequired,// function which determins percent value
percent: PropTypes.number.isRequired,// value of slider
scale: PropTypes.number
};
/**
* Determine if the user is still dragging
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
this._handleFocus = this._handleFocus.bind(this);
this._handleBlur = this._handleBlur.bind(this);
this._handleChange = this._handleChange.bind(this);
this._keyup = this._keyup.bind(this);
this.state = this._getInitialState();
}
/**
* Update input value if slider changes will change props/state
* @param {Object} nextProps React Component properites
* @param {Object} nextState React Component state values
*/
componentWillReceiveProps(nextProps, nextState) {
let nextValue = nextProps.percent * nextProps.max;
// See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form
if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) {
this.setState({ inputValue: nextValue });
}
}
/**
* Update slider textbox visibility/values if changes are made to slider
* @param {Object} prevProps React Component properites
* @param {Object} prevState React Component state values
*/
componentDidUpdate(prevProps, prevState) {
if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') {
this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10);
}
if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) {
// they chose a different module
this.setState({ inputValue: this.props.max });
}
if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) {
this.props.onChange(this.state.inputValue / this.props.max);
}
}
/**
* Set initial state for the textbox.
* We may want to rethink this to
* try and make it a stateless component
* @returns {object} React state object with initial values set
*/
_getInitialState() {
return {
divStyle: { display:'none' },
inputStyle: { width:'4em' },
labelStyle: { marginLeft: '.1em' },
maxLength:5,
size:5,
min:0,
tabIndex:-1,
type:'number',
readOnly: true,
inputValue: this.props.percent * this.props.max
};
}
/**
*
* @param {string} val block or none
*/
_setDisplay(val) {
this.setState({
divStyle: { display:val }
});
}
/**
* Update the input value
* when textbox gets focus
*/
_handleFocus() {
this.setState({
inputValue:this._getValue()
});
}
/**
* Update inputValue when textbox loses focus
*/
_handleBlur() {
this._setDisplay('none');
if (this.state.inputValue !== '') {
this.props.onChange(this.state.inputValue / this.props.max);
} else {
this.setState({
inputValue: this.props.percent * this.props.max
});
}
}
/**
* Get the value in the text box
* @returns {number} inputValue Value of the input box
*/
_getValue() {
return this.state.inputValue;
}
/**
* Update and set limits on input box
* values depending on what user
* has selected
*
* @param {SyntheticEvent} event ReactJs onChange event
*/
_handleChange(event) {
if (event.target.value < 0) {
this.setState({ inputValue: 0 });
} else if (event.target.value <= this.props.max) {
this.setState({ inputValue: event.target.value });
} else {
this.setState({ inputValue: this.props.max });
}
}
/**
* Key up handler for input field.
* If user hits Enter key, blur/close the input field
* @param {Event} event Keyboard event
*/
_keyup(event) {
switch (event.key) {
case 'Enter':
this.sliderVal.blur();
return;
default:
return;
}
}
/**
* Get the value in the text box
* @return {React.Component} Text Input component for Slider
*/
render() {
let { axisUnit, onChange, percent, scale } = this.props;
return <div style={this.state.divStyle}><input style={this.state.inputStyle} value={this._getValue()} min={this.state.min} max={this.props.max} onChange={this._handleChange} onKeyUp={this._keyup} tabIndex={this.state.tabIndex} maxLength={this.state.maxLength} size={this.state.size} onBlur={() => {this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/><text className="primary upp" style={this.state.labelStyle}>{this.props.axisUnit}</text></div>;
}
}

View File

@@ -9,7 +9,7 @@ import { diffDetails } from '../utils/SlotFunctions';
import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions'; import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions'; import { blueprintTooltip } from '../utils/BlueprintFunctions';
import { Module } from 'ed-forge'; import { Module } from 'ed-forge';
import { REG_MILITARY_SLOT, REG_HARDPOINT_SLOT } from 'ed-forge/lib/data/slots'; import { TYPES } from 'ed-forge/lib/data/slots';
import autoBind from 'auto-bind'; import autoBind from 'auto-bind';
import { toPairs } from 'lodash'; import { toPairs } from 'lodash';
@@ -79,7 +79,7 @@ export default class Slot extends TranslatedComponent {
if (m.isEmpty()) { if (m.isEmpty()) {
return <div className="empty"> return <div className="empty">
{translate( {translate(
m.getSlot().match(REG_MILITARY_SLOT) ? 'emptyrestricted' : 'empty' m.isOnSlot(TYPES.MILITARY) ? 'emptyrestricted' : 'empty'
)} )}
</div>; </div>;
} else { } else {
@@ -174,7 +174,7 @@ export default class Slot extends TranslatedComponent {
case size === 0: case size === 0:
// This can also happen for armour but that case was handled above // This can also happen for armour but that case was handled above
return 'U'; return 'U';
case Boolean(m.getSlot().match(REG_HARDPOINT_SLOT)): case m.isOnSlot(TYPES.HARDPOINT):
return HARDPOINT_SLOT_LABELS[size]; return HARDPOINT_SLOT_LABELS[size];
default: default:
return size; return size;