Compare commits

...

56 Commits

Author SHA1 Message Date
Felix Linker
c73ce1c234 Fix: don't cast mount symbol in available modules to string 2021-05-10 21:13:02 +02:00
Felix Linker
00d3a93b91 Re-design shipyard page
Closes #577
2021-05-09 20:09:15 +02:00
Felix Linker
d4e612cb61 Display module selection for alloys correctly
Closes #579
2021-05-09 18:42:37 +02:00
Felix Linker
c07cfc6e70 Don't round integers 2021-02-01 22:52:04 +01:00
Felix Linker
c4c6d32a5d Skip properties that do not apply to a module in blueprint tooltip 2021-02-01 22:41:41 +01:00
Felix Linker
a65bb06754 Implement tooltips for experimental effects 2021-02-01 22:40:01 +01:00
Felix Linker
23548e7c5c Remove redundant code 2021-02-01 22:14:22 +01:00
Felix Linker
74e6f54e19 Improve blueprint tooltips 2021-02-01 22:10:09 +01:00
Felix Linker
a46f8f97f6 Show blueprint grade in ascending order 2021-02-01 22:02:37 +01:00
Felix Linker
44dbdb1703 Don't show mass twice and allow to disable showing mass 2021-02-01 21:56:35 +01:00
Felix Linker
187c5dae4a Implement blueprint tooltips 2021-01-10 22:57:33 +01:00
Felix Linker
07d324a3fa Add key to property header in ModificationsMenu 2021-01-10 22:02:00 +01:00
Felix Linker
5c63afd96c Add stats mapper to show certain synthetic props in ModificationsMenu 2021-01-10 22:01:21 +01:00
Felix Linker
53c40ac9c4 Fix indentation 2021-01-10 21:59:22 +01:00
Felix Linker
a180cbfdd4 Fix: showProp is not required in <Modification> 2021-01-10 21:58:35 +01:00
Felix Linker
fc1524a943 Support non-percent modifiers 2021-01-03 12:32:01 +01:00
Felix Linker
c3747e4e5e Add toggle for properties in overview 2021-01-03 10:17:10 +01:00
Felix Linker
ab153981c9 Rework Modifictaions(Menu) 2021-01-02 10:59:58 +01:00
Felix Linker
a970e052c1 Update react-number-editor
Mitigates a bug where non-rounded numbers were shown to the user.
2021-01-02 10:55:16 +01:00
Felix Linker
df7e264a02 Use package-lock.json
Otherwise `npm audit` does not work.
2021-01-02 10:54:44 +01:00
Felix Linker
cdcda004f3 Implement units in modifications menu 2020-12-31 18:54:36 +01:00
Felix Linker
20e448fc0a Optimize constructors 2020-12-29 16:39:18 +01:00
Felix Linker
436e626c42 Group module selection by category 2020-12-29 16:37:28 +01:00
Felix Linker
3dd4675a0b Hide searchbar for core internal modules 2020-12-29 16:36:43 +01:00
Felix Linker
d3766d9e17 Migrate ed-forge API change 2020-12-29 13:18:57 +01:00
Felix Linker
d987c08ac8 Ship summary key for ship type is id 2020-12-29 12:23:23 +01:00
Felix Linker
f865ef6c6c Fix retrofit cost 2020-11-01 19:54:48 +01:00
Felix Linker
f2b7daac82 MovementProfile code includes pips for re-render on state change 2020-11-01 18:29:04 +01:00
Felix Linker
832bc488b6 Fix excluding modules from costs 2020-11-01 18:25:28 +01:00
Felix Linker
f513166d6c Rework boost button 2020-11-01 17:56:33 +01:00
Felix Linker
61fd0eb991 Rework profiles section 2020-11-01 16:01:53 +01:00
Felix Linker
1e51e7d4a6 Rework WeaponDamageChart 2020-11-01 15:58:28 +01:00
Felix Linker
4943d36bb8 Rework FSD profile 2020-11-01 02:59:37 +01:00
Felix Linker
9271d1fa09 Rework Movement graph 2020-11-01 02:32:52 +01:00
Felix Linker
8a09d94dfa Rework EngineProfile 2020-11-01 02:19:59 +01:00
Felix Linker
c44925dd62 Show normal speed in summary 2020-10-31 13:03:10 +01:00
Felix Linker
d006bbcb0f Handle no shield 2020-10-31 13:02:51 +01:00
Felix Linker
14453f6b80 Rework defence tab 2020-10-24 17:47:17 +02:00
felixlinker
8c267150a9 Rework Offence tab 2020-08-13 20:20:22 +02:00
felixlinker
ed60a78be0 Priority groups are zero indexed 2020-08-09 19:00:57 +02:00
felixlinker
82142b0cb1 Use Module.setEnabled in Power Management table 2020-08-09 19:00:45 +02:00
felixlinker
d8949fedb2 Rework PIP menu 2020-08-09 18:39:39 +02:00
felixlinker
cf72bd11a8 Replace manual bind(this) calls with auto-bind 2020-08-09 17:41:15 +02:00
Felix Linker
16ef7ea389 Rework cost section 2020-04-30 14:25:07 +02:00
Felix Linker
ff455e349e Implement coding standards 2020-04-16 15:22:07 +02:00
Felix Linker
ba9e7f1a32 Add code props variable as ship hash to update changes 2020-04-16 15:12:38 +02:00
Felix Linker
904498b20c Make power stats working 2020-04-15 19:59:50 +02:00
Felix Linker
409be7374c Make outfitting page working 2020-04-10 13:19:53 +02:00
Felix Linker
00c525e6ab Update shipyard-page to ed-forge 2020-04-10 12:50:56 +02:00
felixlinker
a2f52c03a1 Move hardpoint class for module selection into leaves of HTML tree 2019-10-10 18:18:18 +02:00
felixlinker
037df6b166 Remove InternalSlot component 2019-10-10 16:42:14 +02:00
felixlinker
90ab5b4b0a Remove component HardpointSlot 2019-10-10 16:30:51 +02:00
felixlinker
7bbfa8c43f Remove component StandardSlot 2019-10-10 16:11:54 +02:00
felixlinker
2fbcd158cc Rewrite ModificationsMenu 2019-10-10 16:06:41 +02:00
felixlinker
33c201800e Rewrite AvailableModulesMenu.jsx 2019-10-08 22:53:31 +02:00
Felix Linker
9797a8d781 First steps towards ed-forge rewrite 2019-10-08 15:02:16 +02:00
58 changed files with 28132 additions and 5015 deletions

1
.gitignore vendored
View File

@@ -10,4 +10,3 @@ env
.project
.vscode/
docs/
package-lock.json

1
.npmrc
View File

@@ -1 +0,0 @@
package-lock=false

25347
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -123,11 +123,13 @@
"sideEffects": false,
"dependencies": {
"@babel/polyfill": "^7.0.0",
"auto-bind": "^2.1.1",
"browserify-zlib-next": "^1.0.1",
"classnames": "^2.2.6",
"coriolis-data": "../coriolis-data",
"d3": "^5.7.0",
"detect-browser": "^3.0.1",
"ed-forge": "github:EDCD/ed-forge",
"fbemitter": "^2.1.1",
"lodash": "^4.17.11",
"lz-string": "^1.4.4",
@@ -138,7 +140,7 @@
"react-extras": "^0.7.1",
"react-fuzzy": "^0.5.2",
"react-ga": "^2.5.3",
"react-number-editor": "Athanasius/react-number-editor.git#miggy",
"react-number-editor": "^4.0.3",
"recharts": "^1.2.0",
"register-service-worker": "^1.5.2",
"superagent": "^3.8.3"

View File

@@ -5,16 +5,14 @@ import { register } from 'register-service-worker';
import { EventEmitter } from 'fbemitter';
import { getLanguage } from './i18n/Language';
import Persist from './stores/Persist';
import { Ship } from 'ed-forge';
import Announcement from './components/Announcement';
import Header from './components/Header';
import Tooltip from './components/Tooltip';
import ModalExport from './components/ModalExport';
import ModalHelp from './components/ModalHelp';
import ModalImport from './components/ModalImport';
import ModalPermalink from './components/ModalPermalink';
import * as CompanionApiUtils from './utils/CompanionApiUtils';
import * as JournalUtils from './utils/JournalUtils';
import AboutPage from './pages/AboutPage';
import NotFoundPage from './pages/NotFoundPage';
import OutfittingPage from './pages/OutfittingPage';
@@ -22,7 +20,6 @@ import ComparisonPage from './pages/ComparisonPage';
import ShipyardPage from './pages/ShipyardPage';
import ErrorDetails from './pages/ErrorDetails';
const zlib = require('pako');
const request = require('superagent');
/**
@@ -61,7 +58,6 @@ export default class Coriolis extends React.Component {
this._onLanguageChange = this._onLanguageChange.bind(this);
this._onSizeRatioChange = this._onSizeRatioChange.bind(this);
this._keyDown = this._keyDown.bind(this);
this._importBuild = this._importBuild.bind(this);
this.emitter = new EventEmitter();
this.state = {
@@ -93,19 +89,10 @@ export default class Coriolis extends React.Component {
_importBuild(r) {
try {
// Need to decode and gunzip the data, then build the ship
const data = zlib.inflate(new Buffer(r.params.data, 'base64'), { to: 'string' });
const json = JSON.parse(data);
console.info('Ship import data: ');
console.info(json);
let ship;
if (json && json.modules) {
ship = CompanionApiUtils.shipFromJson(json);
} else if (json && json.Modules) {
ship = JournalUtils.shipFromLoadoutJSON(json);
}
r.params.ship = ship.id;
r.params.code = ship.toString();
this._setPage(OutfittingPage, r)
let ship = new Ship(r.params.data);
r.params.ship = ship.getShipType();
r.params.code = ship.compress();
this._setPage(OutfittingPage, r);
} catch (err) {
this._onError('Failed to import ship', r.path, 0, 0, err);
}

View File

@@ -6,7 +6,6 @@ import { autoBind } from 'react-extras';
* Announcement component
*/
export default class Announcement extends React.Component {
static propTypes = {
text: PropTypes.string
};
@@ -27,5 +26,4 @@ export default class Announcement extends React.Component {
render() {
return <div className="announcement" >{this.props.text}</div>;
}
}

View File

@@ -1,120 +1,20 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import TranslatedComponent from './TranslatedComponent';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import cn from 'classnames';
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
import FuzzySearch from 'react-fuzzy';
import { getModuleInfo } from 'ed-forge/lib/data/items';
import { groupBy, mapValues, sortBy } from 'lodash';
import autoBind from 'auto-bind';
const PRESS_THRESHOLD = 500; // mouse/touch down threshold
/*
* Categorisation of module groups
*/
const GRPCAT = {
'sg': 'shields',
'bsg': 'shields',
'psg': 'shields',
'scb': 'shields',
'cc': 'limpet controllers',
'fx': 'limpet controllers',
'hb': 'limpet controllers',
'pc': 'limpet controllers',
'rpl': 'limpet controllers',
'pce': 'passenger cabins',
'pci': 'passenger cabins',
'pcm': 'passenger cabins',
'pcq': 'passenger cabins',
'fh': 'hangars',
'pv': 'hangars',
'fs': 'fuel',
'ft': 'fuel',
'hr': 'structural reinforcement',
'mrp': 'structural reinforcement',
'bl': 'lasers',
'pl': 'lasers',
'ul': 'lasers',
'ml': 'lasers',
'c': 'projectiles',
'mc': 'projectiles',
'axmc': 'experimental',
'fc': 'projectiles',
'rfl': 'experimental',
'pa': 'projectiles',
'rg': 'projectiles',
'mr': 'ordnance',
'axmr': 'experimental',
'rcpl': 'experimental',
'dtl': 'experimental',
'tbsc': 'experimental',
'tbem': 'experimental',
'tbrfl': 'experimental',
'mahr': 'experimental',
'rsl': 'experimental',
'tp': 'ordnance',
'nl': 'ordnance',
'sc': 'scanners',
'ss': 'scanners',
// Utilities
'cs': 'scanners',
'kw': 'scanners',
'ws': 'scanners',
'xs': 'scanners',
'ch': 'defence',
'po': 'defence',
'ec': 'defence',
'sfn': 'defence',
// Guardian
'gpp': 'guardian',
'gpc': 'guardian',
'gsrp': 'guardian',
'ggc': 'guardian',
'gfsb': 'guardian',
'gmrp': 'guardian',
'gsc': 'guardian',
'ghrp': 'guardian',
// Mining
'scl': 'mining',
'pwa': 'mining',
'sdm': 'mining',
// Assists
'dc': 'flight assists',
'sua': 'flight assists',
};
// Order here is the order in which items will be shown in the modules menu
const CATEGORIES = {
// Internals
'am': ['am'],
'cr': ['cr'],
'fi': ['fi'],
'fuel': ['ft', 'fs'],
'hangars': ['fh', 'pv'],
'limpet controllers': ['cc', 'fx', 'hb', 'pc', 'rpl'],
'passenger cabins': ['pce', 'pci', 'pcm', 'pcq'],
'rf': ['rf'],
'shields': ['sg', 'bsg', 'psg', 'scb'],
'structural reinforcement': ['hr', 'mrp'],
'flight assists': ['dc', 'sua'],
// Hardpoints
'lasers': ['pl', 'ul', 'bl'],
'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'],
'ordnance': ['mr', 'tp', 'nl'],
// Utilities
'sb': ['sb'],
'hs': ['hs'],
'defence': ['ch', 'po', 'ec'],
'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners
// Experimental
'experimental': ['axmc', 'axmr', 'rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',],
// Guardian
'guardian': ['gpp', 'gpd', 'gpc', 'ggc', 'gsrp', 'gfsb', 'ghrp', 'gmrp', 'gsc'],
'mining': ['ml', 'scl', 'pwa', 'sdm', 'abl'],
const MOUNT_MAP = {
fixed: <MountFixed className={'lg'} />,
gimbal: <MountGimballed className={'lg'} />,
turret: <MountTurret className={'lg'} />,
};
/**
@@ -122,15 +22,11 @@ const CATEGORIES = {
*/
export default class AvailableModulesMenu extends TranslatedComponent {
static propTypes = {
modules: PropTypes.oneOfType([PropTypes.object, PropTypes.array]).isRequired,
onSelect: PropTypes.func.isRequired,
diffDetails: PropTypes.func,
hideSearch: PropTypes.bool,
m: PropTypes.object,
ship: PropTypes.object.isRequired,
warning: PropTypes.func,
firstSlotId: PropTypes.string,
lastSlotId: PropTypes.string,
activeSlotId: PropTypes.string,
slotDiv: PropTypes.object
};
@@ -141,10 +37,8 @@ export default class AvailableModulesMenu extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
this._hideDiff = this._hideDiff.bind(this);
this._showSearch = this._showSearch.bind(this);
autoBind(this);
this.state = this._initState(props, context);
this.slotItems = [];// Array to hold <li> refs.
}
/**
@@ -154,164 +48,103 @@ export default class AvailableModulesMenu extends TranslatedComponent {
* @return {Object} list: Array of React Components, currentGroup Component if any
*/
_initState(props, context) {
let translate = context.language.translate;
let { m, warning, onSelect, modules, ship } = props;
let list, currentGroup;
const { translate } = context.language;
const { m } = props;
const list = [], fuzzy = [];
let currentGroup;
let buildGroup = this._buildGroup.bind(
this,
ship,
translate,
m,
warning,
(m, event) => {
this._hideDiff(event);
onSelect(m);
}
const modules = m.getApplicableItems().map(getModuleInfo);
const groups = mapValues(
groupBy(modules, (info) => info.meta.group),
(infos) => groupBy(infos, (info) => info.meta.type),
);
let fuzzy = [];
if (modules instanceof Array) {
list = buildGroup(modules[0].grp, modules);
} else {
list = [];
// At present time slots with grouped options (Hardpoints and Internal) can be empty
if (m) {
let emptyId = 'empty';
if (this.firstSlotId == null) this.firstSlotId = emptyId;
let keyDown = this._keyDown.bind(this, onSelect);
list.push(<div className='empty-c upp' key={emptyId} data-id={emptyId} onClick={onSelect.bind(null, null)}
onKeyDown={keyDown} tabIndex="0"
ref={slotItem => this.slotItems[emptyId] = slotItem}>{translate('empty')}</div>);
// Build categories sorted by translated category name
const groupKeys = sortBy(Object.keys(groups), translate);
for (const group of groupKeys) {
const groupName = translate(group);
if (groupKeys.length > 1) {
list.push(<div key={`group-${group}`} className="select-category upp">{groupName}</div>);
}
// Need to regroup the modules by our own categorisation
let catmodules = {};
// Pre-create to preserve ordering
for (let cat in CATEGORIES) {
catmodules[cat] = [];
const categories = groups[group];
const categoryKeys = sortBy(Object.keys(categories), translate);
for (const category of categoryKeys) {
const categoryName = translate(category);
const infos = categories[category];
if (categoryKeys.length > 1) {
list.push(<div key={`category-${category}`} className="select-group cap">{categoryName}</div>);
}
for (let g in modules) {
const moduleCategory = GRPCAT[g] || g;
const existing = catmodules[moduleCategory] || [];
catmodules[moduleCategory] = existing.concat(modules[g]);
}
for (let category in catmodules) {
let categoryHeader = false;
// Order through CATEGORIES if present
const categories = CATEGORIES[category] || [category];
if (categories && categories.length) {
for (let n in categories) {
const grp = categories[n];
// We now have the group and the category. We might not have any modules, though...
if (modules[grp]) {
// Decide if we need a category header as well as a group header
if (categories.length === 1) {
// Show category header instead of group header
if (m && grp == m.grp) {
list.push(<div ref={(elem) => this.groupElem = elem} key={category}
className={'select-category upp'}>{translate(category)}</div>);
} else {
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
}
} else {
// Show category header as well as group header
if (!categoryHeader) {
list.push(<div key={category} className={'select-category upp'}>{translate(category)}</div>);
categoryHeader = true;
}
if (m && grp == m.grp) {
list.push(<div ref={(elem) => this.groupElem = elem} key={grp}
className={'select-group cap'}>{translate(grp)}</div>);
} else {
list.push(<div key={grp} className={'select-group cap'}>{translate(grp)}</div>);
list.push(
this._buildGroup(
m,
category,
infos,
),
);
fuzzy.push(
...infos.map((info) => {
const { meta } = info;
const mount = meta.mount ? ' ' + translate(meta.mount) : '';
return {
grp: groupName,
cat: categoryName,
m: info.proto.Item,
name: `${meta.class}${meta.rating}${mount} ${categoryName}`,
};
}),
);
}
}
list.push(buildGroup(grp, modules[grp]));
for (const i of modules[grp]) {
let mount = '';
if (i.mount === 'F') {
mount = 'Fixed';
} else if (i.mount === 'G') {
mount = 'Gimballed';
} else if (i.mount === 'T') {
mount = 'Turreted';
}
const fuzz = { grp, m: i, name: `${i.class}${i.rating}${mount ? ' ' + mount : ''} ${translate(grp)}` };
fuzzy.push(fuzz);
}
}
}
}
}
}
let trackingFocus = false;
return { list, currentGroup, fuzzy, trackingFocus };
return { list, currentGroup, fuzzy, trackingFocus: false };
}
/**
* Generate React Components for Module Group
* @param {Ship} ship Ship the selection is for
* @param {Function} translate Translate function
* @param {Object} mountedModule Mounted Module
* @param {Function} warningFunc Warning function
* @param {function} onSelect Select/Mount callback
* @param {string} grp Group name
* @param {String} category Category key
* @param {Array} modules Available modules
* @return {React.Component} Available Module Group contents
*/
_buildGroup(ship, translate, mountedModule, warningFunc, onSelect, grp, modules) {
let prevClass = null, prevRating = null, prevName;
let elems = [];
_buildGroup(mountedModule, category, modules) {
const { warning } = this.props;
const ship = mountedModule.getShip();
const classMapping = groupBy(modules, (info) => info.meta.class);
const sortedModules = modules.sort(this._moduleOrder);
const itemsPerClass = Math.max(
...Object.values(classMapping).map((l) => l.length),
);
const itemsPerRow = itemsPerClass <= 2 ? 6 : itemsPerClass;
// Nested array of <li> elements; will be flattened before being rendered.
// Each sub-array represents one row in the final view.
const elems = [[]];
// Calculate the number of items per class. Used so we don't have long lists with only a few items in each row
const tmp = sortedModules.map((v, i) => v['class']).reduce((count, cls) => {
count[cls] = ++count[cls] || 1;
return count;
}, {});
const itemsPerClass = Math.max.apply(null, Object.keys(tmp).map(key => tmp[key]));
// Reverse sort for descending order of module class
for (const clazz of Object.keys(classMapping).sort().reverse()) {
for (let info of sortBy(
classMapping[clazz],
(info) => info.meta.mount || info.meta.rating,
)) {
const { meta } = info;
const { Item } = info.proto;
let itemsOnThisRow = 0;
for (let i = 0; i < sortedModules.length; i++) {
let m = sortedModules[i];
let mount = null;
let disabled = false;
prevName = m.name;
if (ModuleUtils.isShieldGenerator(m.grp)) {
// Shield generators care about maximum hull mass
disabled = ship.hullMass > m.maxmass;
// If the mounted module is experimental as well, we can replace it so
// the maximum does not apply
} else if (m.experimental && (!mountedModule || !mountedModule.experimental)) {
disabled = 4 <= ship.hardpoints.filter(o => o.m && o.m.experimental).length;
// Can only be true if shieldgenmaximalmass is defined, i.e. this
// module must be a shield generator
let disabled = info.props.shieldgenmaximalmass < ship.readProp('hullmass');
if (meta.experimental && !mountedModule.readMeta('experimental')) {
disabled =
4 <=
ship.getHardpoints().filter((m) => m.readMeta('experimental'))
.length;
}
let active = mountedModule && mountedModule.id === m.id;
let classes = cn(m.name ? 'lc' : 'c', {
warning: !disabled && warningFunc && warningFunc(m),
active,
disabled
});
let eventHandlers;
if (disabled) {
eventHandlers = {
onKeyDown: this._keyDown.bind(this, null),
onKeyUp: this._keyUp.bind(this, null)
// Default event handlers for objects that are disabled
let eventHandlers = {};
if (!disabled) {
const showDiff = this._showDiff.bind(this, mountedModule, info);
const select = (event) => {
this._hideDiff(event);
this.props.onSelect(Item);
};
} else {
/**
* Get the ids of the first and last <li> elements in the <ul> that are focusable (i.e. are not active or disabled)
* Will be used to keep focus inside the <ul> on Tab and Shift-Tab while it is visible
*/
if (this.firstSlotId == null) this.firstSlotId = sortedModules[i].id;
if (active) this.activeSlotId = sortedModules[i].id;
this.lastSlotId = sortedModules[i].id;
let showDiff = this._showDiff.bind(this, mountedModule, m);
let select = onSelect.bind(null, m);
eventHandlers = {
onMouseEnter: this._over.bind(this, showDiff),
@@ -319,67 +152,67 @@ export default class AvailableModulesMenu extends TranslatedComponent {
onTouchEnd: this._touchEnd.bind(this, select),
onMouseLeave: this._hideDiff,
onClick: select,
onKeyDown: this._keyDown.bind(this, select),
onKeyUp: this._keyUp.bind(this, 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 (m.name && m.name === prevName) {
// elems.push(<br key={'b' + m.grp + i} />);
itemsOnThisRow = 0;
}
if (itemsOnThisRow == 6 || i > 0 && sortedModules.length > 3 && itemsPerClass > 2 && m.class != prevClass && (m.rating != prevRating || m.mount)) {
elems.push(<br key={'b' + m.grp + i}/>);
itemsOnThisRow = 0;
}
let tbIdx = (classes.indexOf('disabled') < 0) ? 0 : undefined;
elems.push(
<li key={m.id} data-id={m.id} className={classes} {...eventHandlers} tabIndex={tbIdx}
ref={slotItem => this.slotItems[m.id] = slotItem}>
{mount}
{(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')}
</li>
const mountSymbol = MOUNT_MAP[meta.mount];
const li = (
<li key={Item} data-id={Item}
ref={Item === mountedModule.getItem() ? (ref) => { this.activeSlotRef = ref; } : undefined}
className={cn(meta.type === 'armour' ? 'lc' : 'c', {
warning: !disabled && warning && warning(info),
active: mountedModule.getItem() === Item,
disabled,
hardpoint: mountSymbol,
})}
{...eventHandlers}
>{mountSymbol}{meta.type === 'armour' ? Item : `${meta.class}${meta.rating}`}</li>
);
itemsOnThisRow++;
prevClass = m.class;
prevRating = m.rating;
prevName = m.name;
const tail = elems.pop();
let newTail = [tail];
if (tail.length < itemsPerRow) {
// If the row has not grown too long, the new <li> element can be
// added to the row itself
tail.push(li);
} else {
// Otherwise, the last row gets a line break element added and this
// item is put into a new row
tail.push(<br key={elems.length}/>);
newTail.push([li]);
}
return <ul key={'modules' + grp}>{elems}</ul>;
elems.push(...newTail);
}
}
return <ul key={'ul' + category}>{[].concat(...elems)}</ul>;
}
/**
* Generate tooltip content for the difference between the
* mounted module and the hovered modules
* @param {Object} mm The module mounet currently
* @param {Object} m The hovered module
* @param {Object} mountedModule The module mounted currently
* @param {Object} hoveringModule The hovered module
* @param {DOMRect} rect DOMRect for target element
*/
_showDiff(mm, m, rect) {
_showDiff(mountedModule, hoveringModule, rect) {
if (this.props.diffDetails) {
this.touchTimeout = null;
this.context.tooltip(this.props.diffDetails(m, mm), rect);
// TODO:
// this.context.tooltip(
// this.props.diffDetails(hoveringModule, mountedModule),
// rect,
// );
}
}
/**
* Generate tooltip content for the difference between the
* mounted module and the hovered modules
* @returns {React.Component} Search component if available
*/
_showSearch() {
if (this.props.modules instanceof Array) {
if (this.props.hideSearch) {
return;
}
return (
@@ -442,41 +275,6 @@ export default class AvailableModulesMenu extends TranslatedComponent {
this._hideDiff();
}
/**
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
* @param {Function} select Select module callback
* @param {SyntheticEvent} event Event
*/
_keyDown(select, event) {
let className = event.currentTarget.attributes['class'].value;
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
select();
return;
}
let elemId = event.currentTarget.attributes['data-id'].value;
if (className.indexOf('disabled') < 0 && event.key == 'Tab') {
if (event.shiftKey && elemId == this.firstSlotId) {
event.preventDefault();
this.slotItems[this.lastSlotId].focus();
return;
}
if (!event.shiftKey && elemId == this.lastSlotId) {
event.preventDefault();
this.slotItems[this.firstSlotId].focus();
return;
}
}
}
/**
* Key Up
* @param {Function} select Select module callback
* @param {SytheticEvent} event Event
*/
_keyUp(select, event) {
// nothing here yet
}
/**
* Hide diff tooltip
* @param {SyntheticEvent} event Event
@@ -487,60 +285,12 @@ export default class AvailableModulesMenu extends TranslatedComponent {
this.context.tooltip();
}
/**
* Order two modules suitably for display in module selection
* @param {Object} a the first module
* @param {Object} b the second module
* @return {int} -1 if the first module should go first, 1 if the second module should go first
*/
_moduleOrder(a, b) {
// Named modules go last
if (!a.name && b.name) {
return -1;
}
if (a.name && !b.name) {
return 1;
}
// Class ordered from highest (8) to lowest (1)
if (a.class < b.class) {
return 1;
}
if (a.class > b.class) {
return -1;
}
// Mount type, if applicable
if (a.mount && b.mount && a.mount !== b.mount) {
if (a.mount === 'F' || (a.mount === 'G' && b.mount === 'T')) {
return -1;
} else {
return 1;
}
}
// Rating ordered from highest (A) to lowest (E)
if (a.rating < b.rating) {
return -1;
}
if (a.rating > b.rating) {
return 1;
}
// Do not attempt to order by name at this point, as that mucks up the order of armour
return 0;
}
/**
* Scroll to mounted (if it exists) module group on mount
*/
componentDidMount() {
if (this.groupElem) { // Scroll to currently selected group
this.node.scrollTop = this.groupElem.offsetTop;
}
/**
* Set focus on active or first slot element, if applicable.
*/
if (this.slotItems[this.activeSlotId]) {
this.slotItems[this.activeSlotId].focus();
} else if (this.slotItems[this.firstSlotId]) {
this.slotItems[this.firstSlotId].focus();
if (this.activeSlotRef) {
this.activeSlotRef.focus();
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import autoBind from 'auto-bind';
/**
* Boost displays a boost button that toggles bosot
@@ -8,8 +9,6 @@ import TranslatedComponent from './TranslatedComponent';
*/
export default class Boost extends TranslatedComponent {
static propTypes = {
marker: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
boost: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
@@ -19,12 +18,9 @@ export default class Boost extends TranslatedComponent {
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
constructor(props) {
super(props);
const { ship, boost } = props;
this._keyDown = this._keyDown.bind(this);
this._toggleBoost = this._toggleBoost.bind(this);
autoBind(this);
}
/**
@@ -70,13 +66,12 @@ export default class Boost extends TranslatedComponent {
* @return {React.Component} contents
*/
render() {
const { formats, translate, units } = this.context.language;
const { ship, boost } = this.props;
// TODO disable if ship cannot boost
const { translate } = this.context.language;
return (
<span id='boost'>
<button id='boost' className={boost ? 'selected' : null} onClick={this._toggleBoost}>{translate('boost')}</button>
<button id='boost' className={this.props.boost ? 'selected' : null} onClick={this._toggleBoost}>
{translate('boost')}
</button>
</span>
);
}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import Slider from '../components/Slider';
import autoBind from 'auto-bind';
/**
* Cargo slider
@@ -21,8 +22,7 @@ export default class Cargo extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
this._cargoChange = this._cargoChange.bind(this);
autoBind(this);
}
/**

View File

@@ -1,13 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import { Ships } from 'coriolis-data/dist';
import Persist from '../stores/Persist';
import Ship from '../shipyard/Ship';
import { Factory, Ship } from 'ed-forge';
import { Insurance } from '../shipyard/Constants';
import { slotName, slotComparator } from '../utils/SlotFunctions';
import TranslatedComponent from './TranslatedComponent';
import { ShoppingIcon } from '../components/SvgIcons';
import { ShoppingIcon } from './SvgIcons';
import autoBind from 'auto-bind';
import { assign, differenceBy, sortBy, reverse } from 'lodash';
import { FUEL_CAPACITY } from 'ed-forge/lib/ship-stats';
/**
* Cost Section
@@ -16,7 +17,7 @@ export default class CostSection extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
code: PropTypes.string.isRequired,
buildName: PropTypes.string
buildName: PropTypes.string,
};
/**
@@ -25,71 +26,34 @@ export default class CostSection extends TranslatedComponent {
*/
constructor(props) {
super(props);
this._costsTab = this._costsTab.bind(this);
this._sortCost = this._sortCost.bind(this);
this._sortAmmo = this._sortAmmo.bind(this);
this._sortRetrofit = this._sortRetrofit.bind(this);
this._buildRetrofitShip = this._buildRetrofitShip.bind(this);
this._onBaseRetrofitChange = this._onBaseRetrofitChange.bind(this);
this._defaultRetrofitName = this._defaultRetrofitName.bind(this);
this._eddbShoppingList = this._eddbShoppingList.bind(this);
let data = Ships[props.ship.id]; // Retrieve the basic ship properties, slots and defaults
let retrofitName = this._defaultRetrofitName(props.ship.id, props.buildName);
let retrofitShip = this._buildRetrofitShip(props.ship.id, retrofitName);
let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount();
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
autoBind(this);
const { ship, buildName } = props;
this.shipType = ship.getShipType();
this.state = {
retrofitShip,
retrofitName,
shipDiscount,
moduleDiscount,
retrofitName: Persist.hasBuild(ship.getShipType(), buildName) ? buildName : null,
shipDiscount: Persist.getShipDiscount(),
moduleDiscount: Persist.getModuleDiscount(),
insurance: Insurance[Persist.getInsurance()],
tab: Persist.getCostTab(),
buildOptions: Persist.getBuildsNamesFor(props.ship.id),
ammoPredicate: 'cr',
ammoDesc: true,
costPredicate: 'cr',
costDesc: true,
retroPredicate: 'cr',
retroDesc: true
buildOptions: Persist.getBuildsNamesFor(ship.getShipType()),
predicate: 'cr',
desc: true,
excluded: {},
};
}
/**
* Create a ship instance to base/reference retrofit changes from
* @param {string} shipId Ship Id
* @param {string} name Build name
* @param {Ship} retrofitShip Existing retrofit ship
* @return {Ship} Retrofit ship
*/
_buildRetrofitShip(shipId, name, retrofitShip) {
let data = Ships[shipId]; // Retrieve the basic ship properties, slots and defaults
if (!retrofitShip) { // Don't create a new instance unless needed
retrofitShip = new Ship(shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison
}
if (Persist.hasBuild(shipId, name)) {
retrofitShip.buildFrom(Persist.getBuild(shipId, name)); // Populate modules from existing build
_buildRetrofitShip() {
const { retrofitName } = this.state;
if (Persist.hasBuild(this.shipType, retrofitName)) {
return new Ship(Persist.getBuild(this.shipType, retrofitName));
} else {
retrofitShip.buildWith(data.defaults); // Populate with default components
return Factory.newShip(this.shipType);
}
return retrofitShip;
}
/**
* Get the default retrofit build name if it exists
* @param {string} shipId Ship Id
* @param {string} name Build name
* @return {string} Build name or null
*/
_defaultRetrofitName(shipId, name) {
return Persist.hasBuild(shipId, name) ? name : null;
}
/**
@@ -107,9 +71,6 @@ export default class CostSection extends TranslatedComponent {
_onDiscountChanged() {
let shipDiscount = Persist.getShipDiscount();
let moduleDiscount = Persist.getModuleDiscount();
this.props.ship.applyDiscounts(shipDiscount, moduleDiscount);
this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount);
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
this.setState({ shipDiscount, moduleDiscount });
}
@@ -126,156 +87,33 @@ export default class CostSection extends TranslatedComponent {
* @param {SyntheticEvent} event Build name to base the retrofit ship on
*/
_onBaseRetrofitChange(event) {
let retrofitName = event.target.value;
let ship = this.props.ship;
if (retrofitName) {
this.state.retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName));
} else {
this.state.retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
}
this._updateRetrofit(ship, this.state.retrofitShip);
this.setState({ retrofitName });
this.setState({ retrofitName: event.target.value });
}
/**
* On builds changed check to see if the retrofit ship needs
* to be updated
* Toggle item cost inclusion
* @param {String} key Key of the row to toggle
*/
_onBuildsChanged() {
let update = false;
let ship = this.props.ship;
let { retrofitName, retrofitShip } = this.state;
if(!Persist.hasBuild(ship.id, retrofitName)) {
retrofitShip.buildWith(Ships[ship.id].defaults); // Retrofit ship becomes stock build
this.setState({ retrofitName: null });
update = true;
} else if (Persist.getBuild(ship.id, retrofitName) != retrofitShip.toString()) {
retrofitShip.buildFrom(Persist.getBuild(ship.id, retrofitName)); // Repopulate modules from saved build
update = true;
}
if (update) { // Update retrofit comparison
this._updateRetrofit(ship, retrofitShip);
}
// Update list of retrofit base build options
this.setState({ buildOptions: Persist.getBuildsNamesFor(ship.id) });
_toggleExcluded(key) {
let { excluded } = this.state;
excluded = assign({}, excluded);
const slotExcluded = excluded[key];
excluded[key] = (slotExcluded === undefined ? true : !slotExcluded);
this.setState({ excluded });
}
/**
* Toggle item cost inclusion in overall total
* @param {Object} item Cost item
* Set list sort predicate
* @param {string} newPredicate sort predicate
*/
_toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost);
this.forceUpdate();
_sortBy(newPredicate) {
let { predicate, desc } = this.state;
if (newPredicate == predicate) {
desc = !desc;
}
/**
* Toggle item cost inclusion in retrofit total
* @param {Object} item Cost item
*/
_toggleRetrofitCost(item) {
let retrofitTotal = this.state.retrofitTotal;
item.retroItem.incCost = !item.retroItem.incCost;
retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1);
this.setState({ retrofitTotal });
}
/**
* Set cost list sort predicate
* @param {string} predicate sort predicate
*/
_sortCostBy(predicate) {
let { costPredicate, costDesc } = this.state;
if (costPredicate == predicate) {
costDesc = !costDesc;
}
this.setState({ costPredicate: predicate, costDesc });
}
/**
* Sort cost list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortCost(ship, predicate, desc) {
let costList = ship.costList;
let translate = this.context.language.translate;
if (predicate == 'm') {
costList.sort(slotComparator(translate, null, desc));
} else {
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
}
}
/**
* Set ammo list sort predicate
* @param {string} predicate sort predicate
*/
_sortAmmoBy(predicate) {
let { ammoPredicate, ammoDesc } = this.state;
if (ammoPredicate == predicate) {
ammoDesc = !ammoDesc;
}
this.setState({ ammoPredicate: predicate, ammoDesc });
}
/**
* Sort ammo cost list
* @param {Array} ammoCosts Ammo cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortAmmo(ammoCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'm') {
ammoCosts.sort(slotComparator(translate, null, desc));
} else {
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
}
}
/**
* Set retrofit list sort predicate
* @param {string} predicate sort predicate
*/
_sortRetrofitBy(predicate) {
let { retroPredicate, retroDesc } = this.state;
if (retroPredicate == predicate) {
retroDesc = !retroDesc;
}
this.setState({ retroPredicate: predicate, retroDesc });
}
/**
* Sort retrofit cost list
* @param {Array} retrofitCosts Retrofit cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortRetrofit(retrofitCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'cr') {
retrofitCosts.sort((a, b) => a.netCost - b.netCost);
} else {
retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : ''));
}
if (!desc) {
retrofitCosts.reverse();
}
this.setState({ predicate: newPredicate, desc });
}
/**
@@ -284,18 +122,34 @@ export default class CostSection extends TranslatedComponent {
*/
_costsTab() {
let { ship } = this.props;
let { shipDiscount, moduleDiscount, insurance } = this.state;
let {
excluded, shipDiscount, moduleDiscount, insurance, desc, predicate
} = this.state;
let { translate, formats, units } = this.context.language;
let rows = [];
for (let i = 0, l = ship.costList.length; i < l; i++) {
let item = ship.costList[i];
if (item.m && item.m.cost) {
let toggle = this._toggleCost.bind(this, item);
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.incCost })}>
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{item.m.class + item.m.rating}</td>
<td className='le ptr shorten cap' onClick={toggle}>{slotName(translate, item)}</td>
<td className='ri ptr' onClick={toggle}>{formats.int(item.discountedCost)}{units.CR}</td>
let modules = sortBy(
ship.getModules(),
(predicate === 'm' ? (m) => m.getItem() : (m) => m.readMeta('cost'))
);
if (desc) {
reverse(modules);
}
let totalCost = 0;
for (let module of modules) {
const cost = module.readMeta('cost');
const slot = module.getSlot();
if (cost) {
let toggle = this._toggleExcluded.bind(this, slot);
const disabled = excluded[slot];
if (!disabled) {
totalCost += cost;
}
rows.push(<tr key={slot} className={cn('highlight', { disabled })}>
<td className='ptr' style={{ width: '1em' }} onClick={toggle}>{module.getClassRating()}</td>
<td className='le ptr shorten cap' onClick={toggle}>{translate(module.readMeta('type'))}</td>
<td className='ri ptr' onClick={toggle}>{formats.int(cost * (1 - moduleDiscount))}{units.CR}</td>
</tr>);
}
}
@@ -304,23 +158,23 @@ export default class CostSection extends TranslatedComponent {
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortCostBy.bind(this,'m')}>
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>
{translate('module')}
{shipDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('ship')} -${formats.pct(shipDiscount)}]`}</u> : null}
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
</th>
<th className='sortable le' onClick={this._sortCostBy.bind(this, 'cr')} >{translate('credits')}</th>
<th className='sortable le' onClick={() => this._sortBy('cr')} >{translate('credits')}</th>
</tr>
</thead>
<tbody>
{rows}
<tr className='ri'>
<td colSpan='2' className='lbl' >{translate('total')}</td>
<td className='val'>{formats.int(ship.totalCost)}{units.CR}</td>
<td className='val'>{formats.int(totalCost)}{units.CR}</td>
</tr>
<tr className='ri'>
<td colSpan='2' className='lbl'>{translate('insurance')}</td>
<td className='val'>{formats.int(ship.totalCost * insurance)}{units.CR}</td>
<td className='val'>{formats.int(totalCost * insurance)}{units.CR}</td>
</tr>
</tbody>
</table>
@@ -331,14 +185,63 @@ export default class CostSection extends TranslatedComponent {
* Open up a window for EDDB with a shopping list of our retrofit components
*/
_eddbShoppingList() {
const { retrofitCosts } = this.state;
const {} = this.state;
const { ship } = this.props;
// Provide unique list of non-PP module EDDB IDs to buy
const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
// const modIds = retrofitCosts.filter(item => item.retroItem.incCost && item.buyId && !item.buyPp).map(item => item.buyId).filter((v, i, a) => a.indexOf(v) === i);
// Open up the relevant URL
window.open('https://eddb.io/station?m=' + modIds.join(','));
// TODO:
// window.open('https://eddb.io/station?m=' + modIds.join(','));
}
/**
*
*/
_retrofitInfo() {
const { ship } = this.props;
const { desc, moduleDiscount, predicate, retrofitName, excluded } = this.state;
const retrofitShip = this._buildRetrofitShip();
const currentModules = ship.getModules();
const oldModules = retrofitShip.getModules();
const buyModules = differenceBy(currentModules, oldModules, (m) => m.getItem());
const sellModules = differenceBy(oldModules, currentModules, (m) => m.getItem());
let modules = [];
let totalCost = 0;
const addModule = (m, costFactor) => {
const key = `${m.getItem()}@${m.getSlot()}`;
const cost = costFactor * m.readMeta('cost') * (1 - moduleDiscount);
modules.push({
key, cost,
rating: m.getClassRating(),
item: m.readMeta('type'),
});
if (!excluded[key]) {
totalCost += cost;
}
};
for (let m of buyModules) {
addModule(m, 1);
}
for (let m of sellModules) {
addModule(m, -1);
}
let _sortF = undefined;
switch (predicate) {
case 'cr': _sortF = (o) => o.cost; break;
case 'm':
default: _sortF = (o) => o.item; break;
};
modules = sortBy(modules, _sortF);
if (desc) {
reverse(modules);
}
return [totalCost, modules];
}
/**
@@ -346,59 +249,52 @@ export default class CostSection extends TranslatedComponent {
* @return {React.Component} Tab contents
*/
_retrofitTab() {
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
let { buildOptions, excluded, moduleDiscount, retrofitName } = this.state;
const { termtip, tooltip } = this.context;
let { translate, formats, units } = this.context.language;
let int = formats.int;
let rows = [], options = [<option key='stock' value=''>{translate('Stock')}</option>];
for (let opt of this.state.buildOptions) {
options.push(<option key={opt} value={opt}>{opt}</option>);
}
if (retrofitCosts.length) {
for (let i = 0, l = retrofitCosts.length; i < l; i++) {
let item = retrofitCosts[i];
rows.push(<tr key={i} className={cn('highlight', { disabled: !item.retroItem.incCost })} onClick={this._toggleRetrofitCost.bind(this, item)}>
<td className='ptr' style={{ width: '1em' }}>{item.sellClassRating}</td>
<td className='le ptr shorten cap'>{translate(item.sellName)}</td>
<td className='ptr' style={{ width: '1em' }}>{item.buyClassRating}</td>
<td className='le ptr shorten cap'>{translate(item.buyName)}</td>
<td colSpan='2' className={cn('ri ptr', item.retroItem.incCost ? item.netCost > 0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}</td>
</tr>);
}
} else {
rows = <tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>;
}
const [retrofitTotal, retrofitInfo] = this._retrofitInfo();
return <div>
<div className='scroll-x'>
<table style={{ width: '100%' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'sellName')}>{translate('sell')}</th>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'buyName')}>{translate('buy')}</th>
<th colSpan='2' className='sortable le' onClick={this._sortRetrofitBy.bind(this, 'cr')}>
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('cr')}>
{translate('net cost')}
{moduleDiscount ? <u className='cap optional-hide' style={{ marginLeft: '0.5em' }}>{`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}</u> : null}
</th>
</tr>
</thead>
<tbody>
{rows}
{retrofitInfo.length ?
retrofitInfo.map((info) => (
<tr key={info.key} className={cn('highlight', { disabled: excluded[info.key] })}
onClick={() => this._toggleExcluded(info.key)}>
<td className='ptr' style={{ width: '1em' }}>{info.rating}</td>
<td className='le ptr shorten cap'>{translate(info.item)}</td>
<td colSpan="2" className={cn('ri ptr', excluded[info.key] ? 'disabled' : (info.cost < 0 ? 'secondary-disabled' : 'warning'))}>
{int(info.cost)}{units.CR}
</td>
</tr>
)) : (
<tr><td colSpan='7' style={{ padding: '3em 0' }}>{translate('PHRASE_NO_RETROCH')}</td></tr>
)}
<tr className='ri'>
<td className='lbl' ><button onClick={this._eddbShoppingList} onMouseOver={termtip.bind(null, 'PHRASE_REFIT_SHOPPING_LIST')} onMouseOut={tooltip.bind(null, null)}><ShoppingIcon className='lg' style={{ fill: 'black' }}/></button></td>
<td colSpan='3' className='lbl' >{translate('cost')}</td>
<td className='lbl' >{translate('cost')}</td>
<td colSpan='2' className={cn('val', retrofitTotal > 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
{int(retrofitTotal)}{units.CR}
</td>
</tr>
<tr className='ri'>
<td colSpan='4' className='lbl cap' >{translate('retrofit from')}</td>
<td colSpan='2' className='lbl cap' >{translate('retrofit from')}</td>
<td className='val cen' style={{ borderRight: 'none', width: '1em' }}><u className='primary-disabled'>&#9662;</u></td>
<td className='val' style={{ borderLeft:'none', padding: 0 }}>
<select style={{ width: '100%', padding: 0 }} value={retrofitName || translate('Stock')} onChange={this._onBaseRetrofitChange}>
{options}
<option key='stock' value=''>{translate('Stock')}</option>
{buildOptions.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
</select>
</td>
</tr>
@@ -408,63 +304,50 @@ export default class CostSection extends TranslatedComponent {
</div>;
}
/**
* Update retrofit costs
* @param {Ship} ship Ship instance
* @param {Ship} retrofitShip Retrofit Ship instance
*
* @param {*} modules
*/
_updateRetrofit(ship, retrofitShip) {
let retrofitCosts = [];
let retrofitTotal = 0, i, l, item;
_ammoInfo() {
const { ship } = this.props;
const { desc, predicate } = this.state;
if (ship.bulkheads.m.index != retrofitShip.bulkheads.m.index) {
item = {
buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating,
buyId: ship.bulkheads.m.eddbID,
buyPp: ship.bulkheads.m.pp,
buyName: ship.bulkheads.m.name,
sellClassRating: retrofitShip.bulkheads.m.class + retrofitShip.bulkheads.m.rating,
sellName: retrofitShip.bulkheads.m.name,
netCost: ship.bulkheads.discountedCost - retrofitShip.bulkheads.discountedCost,
retroItem: retrofitShip.bulkheads
};
retrofitCosts.push(item);
if (retrofitShip.bulkheads.incCost) {
retrofitTotal += item.netCost;
let info = [{
key: 'fuel',
item: 'Fuel',
qty: ship.get(FUEL_CAPACITY),
unitCost: 50,
cost: 50 * ship.get(FUEL_CAPACITY),
}];
for (let m of ship.getModules()) {
const rebuilds = m.get('bays') * m.get('rebuildsperbay');
const ammo = (m.get('ammomaximum') + m.get('ammoclipsize')) || rebuilds;
if (ammo) {
const unitCost = m.readMeta('ammocost');
info.push({
key: `restock_${m.getSlot()}`,
rating: m.getClassRating(),
item: m.readMeta('type'),
qty: ammo,
unitCost, cost: unitCost * ammo,
});
}
}
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
let retroSlotGroup = retrofitShip[g];
let slotGroup = ship[g];
for (i = 0, l = slotGroup.length; i < l; i++) {
const modId = slotGroup[i].m ? slotGroup[i].m.eddbID : null;
const retroModId = retroSlotGroup[i].m ? retroSlotGroup[i].m.eddbID : null;
if (modId != retroModId) {
item = { netCost: 0, retroItem: retroSlotGroup[i] };
if (slotGroup[i].m) {
item.buyId = slotGroup[i].m.eddbID,
item.buyPp = slotGroup[i].m.pp,
item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp;
item.buyClassRating = slotGroup[i].m.class + slotGroup[i].m.rating;
item.netCost = slotGroup[i].discountedCost;
}
if (retroSlotGroup[i].m) {
item.sellName = retroSlotGroup[i].m.name || retroSlotGroup[i].m.grp;
item.sellClassRating = retroSlotGroup[i].m.class + retroSlotGroup[i].m.rating;
item.netCost -= retroSlotGroup[i].discountedCost;
}
retrofitCosts.push(item);
if (retroSlotGroup[i].incCost) {
retrofitTotal += item.netCost;
}
}
let _sortF = undefined;
switch (predicate) {
case 'cr': _sortF = (o) => o.cost; break;
case 'qty': _sortF = (o) => o.qty; break;
case 'cost': _sortF = (o) => o.unitCost; break;
case 'm':
default: _sortF = (o) => o.item;
}
info = sortBy(info, _sortF);
if (desc) {
reverse(info);
}
this.setState({ retrofitCosts, retrofitTotal });
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
return info;
}
/**
@@ -472,20 +355,24 @@ export default class CostSection extends TranslatedComponent {
* @return {React.Component} Tab contents
*/
_ammoTab() {
let { ammoTotal, ammoCosts } = this.state;
let { translate, formats, units } = this.context.language;
let int = formats.int;
let rows = [];
const { excluded } = this.state;
const { translate, formats, units } = this.context.language;
const int = formats.int;
const rows = [];
for (let i = 0, l = ammoCosts.length; i < l; i++) {
let item = ammoCosts[i];
rows.push(<tr key={i} className='highlight'>
<td style={{ width: '1em' }}>{item.m.class + item.m.rating}</td>
<td className='le shorten cap'>{slotName(translate, item)}</td>
<td className='ri'>{int(item.max)}</td>
<td className='ri'>{int(item.cost)}{units.CR}</td>
<td className='ri'>{int(item.total)}{units.CR}</td>
const ammoInfo = this._ammoInfo();
let total = 0;
for (let i of ammoInfo) {
const disabled = excluded[i.key];
rows.push(<tr key={i.key} onClick={() => this._toggleExcluded(i.key)}
className={cn('highlight', { disabled })}>
<td style={{ width: '1em' }}>{i.rating}</td>
<td className='le shorten cap'>{translate(i.item)}</td>
<td className='ri'>{int(i.qty)}</td>
<td className='ri'>{int(i.unitCost)}{units.CR}</td>
<td className='ri'>{int(i.cost)}{units.CR}</td>
</tr>);
total += disabled ? 0 : i.cost;
}
return <div>
@@ -493,17 +380,17 @@ export default class CostSection extends TranslatedComponent {
<table style={{ width: '100%' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'm')} >{translate('module')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'max')} >{translate('qty')}</th>
<th colSpan='1' className='sortable le' onClick={this._sortAmmoBy.bind(this, 'cost')} >{translate('unit cost')}</th>
<th className='sortable le' onClick={this._sortAmmoBy.bind(this, 'total')}>{translate('subtotal')}</th>
<th colSpan='2' className='sortable le' onClick={() => this._sortBy('m')}>{translate('module')}</th>
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('qty')}>{translate('qty')}</th>
<th colSpan='1' className='sortable le' onClick={() => this._sortBy('cost')}>{translate('unit cost')}</th>
<th className='sortable le' onClick={() => this._sortBy('cr')}>{translate('subtotal')}</th>
</tr>
</thead>
<tbody>
{rows}
<tr className='ri'>
<td colSpan='4' className='lbl' >{translate('total')}</td>
<td className='val'>{int(ammoTotal)}{units.CR}</td>
<td className='val'>{int(total)}{units.CR}</td>
</tr>
</tbody>
</table>
@@ -511,103 +398,6 @@ export default class CostSection extends TranslatedComponent {
</div>;
}
/**
* Recalculate all ammo costs
* @param {Ship} ship Ship instance
*/
_updateAmmoCosts(ship) {
let ammoCosts = [], ammoTotal = 0, item, q, limpets = 0, srvs = 0, scoop = false;
for (let g in { standard: 1, internal: 1, hardpoints: 1 }) {
let slotGroup = ship[g];
for (let i = 0, l = slotGroup.length; i < l; i++) {
if (slotGroup[i].m) {
// Special cases needed for SCB, AFMU, and limpet controllers since they don't use standard ammo/clip
q = 0;
switch (slotGroup[i].m.grp) {
case 'fs': // Skip fuel calculation if scoop present
scoop = true;
break;
case 'scb':
q = slotGroup[i].m.getAmmo() + 1;
break;
case 'am':
q = slotGroup[i].m.getAmmo();
break;
case 'pv':
srvs += slotGroup[i].m.getBays();
break;
case 'fx': case 'hb': case 'cc': case 'pc':
limpets = ship.cargoCapacity;
break;
default:
q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo();
}
// Calculate ammo costs only if a cost is specified
if (slotGroup[i].m.ammocost > 0) {
item = {
m: slotGroup[i].m,
max: q,
cost: slotGroup[i].m.ammocost,
total: q * slotGroup[i].m.ammocost
};
ammoCosts.push(item);
ammoTotal += item.total;
}
// Add fighters
if (slotGroup[i].m.grp === 'fh') {
item = {
m: slotGroup[i].m,
max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(),
cost: slotGroup[i].m.fightercost,
total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost
};
ammoCosts.push(item);
ammoTotal += item.total;
}
}
}
}
// Limpets if controllers exist and cargo space available
if (limpets > 0) {
item = {
m: { name: 'limpets', class: '', rating: '' },
max: ship.cargoCapacity,
cost: 101,
total: ship.cargoCapacity * 101
};
ammoCosts.push(item);
ammoTotal += item.total;
}
if (srvs > 0) {
item = {
m: { name: 'SRVs', class: '', rating: '' },
max: srvs,
cost: 1030,
total: srvs * 1030
};
ammoCosts.push(item);
ammoTotal += item.total;
}
// Calculate refuel costs if no scoop present
if (!scoop) {
item = {
m: { name: 'fuel', class: '', rating: '' },
max: ship.fuelCapacity,
cost: 50,
total: ship.fuelCapacity * 50
};
ammoCosts.push(item);
ammoTotal += item.total;
}
this.setState({ ammoTotal, ammoCosts });
this._sortAmmo(ammoCosts, this.state.ammoPredicate, this.state.ammoDesc);
}
/**
* Add listeners on mount and update costs
*/
@@ -615,64 +405,7 @@ export default class CostSection extends TranslatedComponent {
this.listeners = [
Persist.addListener('discounts', this._onDiscountChanged.bind(this)),
Persist.addListener('insurance', this._onInsuranceChanged.bind(this)),
Persist.addListener('builds', this._onBuildsChanged.bind(this)),
];
this._updateAmmoCosts(this.props.ship);
this._updateRetrofit(this.props.ship, this.state.retrofitShip);
this._sortCost(this.props.ship);
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next context
*/
componentWillReceiveProps(nextProps, nextContext) {
let retrofitShip = this.state.retrofitShip;
if (nextProps.ship != this.props.ship) { // Ship has changed
let nextId = nextProps.ship.id;
let retrofitName = this._defaultRetrofitName(nextId, nextProps.buildName);
retrofitShip = this._buildRetrofitShip(nextId, retrofitName, nextId == this.props.ship.id ? retrofitShip : null);
this.setState({
retrofitShip,
retrofitName,
buildOptions: Persist.getBuildsNamesFor(nextId)
});
}
if (nextProps.ship != this.props.ship || nextProps.code != this.props.code) {
nextProps.ship.applyDiscounts(Persist.getShipDiscount(), Persist.getModuleDiscount());
this._updateAmmoCosts(nextProps.ship);
this._updateRetrofit(nextProps.ship, retrofitShip);
this._sortCost(nextProps.ship);
}
}
/**
* Sort lists before render
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextState Incoming/Next state
*/
componentWillUpdate(nextProps, nextState) {
let state = this.state;
switch (nextState.tab) {
case 'ammo':
if (state.ammoPredicate != nextState.ammoPredicate || state.ammoDesc != nextState.ammoDesc) {
this._sortAmmo(nextState.ammoCosts, nextState.ammoPredicate, nextState.ammoDesc);
}
break;
case 'retrofit':
if (state.retroPredicate != nextState.retroPredicate || state.retroDesc != nextState.retroDesc) {
this._sortRetrofit(nextState.retrofitCosts, nextState.retroPredicate, nextState.retroDesc);
}
break;
default:
if (state.costPredicate != nextState.costPredicate || state.costDesc != nextState.costDesc) {
this._sortCost(nextProps.ship, nextState.costPredicate, nextState.costDesc);
}
}
}
/**

View File

@@ -4,6 +4,8 @@ import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations';
import PieChart from './PieChart';
import VerticalBarChart from './VerticalBarChart';
import autoBind from 'auto-bind';
import { ARMOUR_METRICS, MODULE_PROTECTION_METRICS, SHIELD_METRICS } from 'ed-forge/lib/ship-stats';
/**
* Defence information
@@ -15,12 +17,10 @@ import VerticalBarChart from './VerticalBarChart';
*/
export default class Defence extends TranslatedComponent {
static propTypes = {
marker: PropTypes.string.isRequired,
code: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
opponent: PropTypes.object.isRequired,
engagementrange: PropTypes.number.isRequired,
sys: PropTypes.number.isRequired,
opponentWep: PropTypes.number.isRequired
engagementRange: PropTypes.number.isRequired,
};
/**
@@ -29,22 +29,7 @@ export default class Defence extends TranslatedComponent {
*/
constructor(props) {
super(props);
const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(props.ship, props.opponent, props.sys, props.opponentWep, props.engagementrange);
this.state = { shield, armour, shielddamage, armourdamage };
}
/**
* Update the state if our properties change
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
if (this.props.marker != nextProps.marker || this.props.sys != nextProps.sys) {
const { shield, armour, shielddamage, armourdamage } = Calc.defenceMetrics(nextProps.ship, nextProps.opponent, nextProps.sys, nextProps.opponentWep, nextProps.engagementrange);
this.setState({ shield, armour, shielddamage, armourdamage });
}
return true;
autoBind(this);
}
/**
@@ -52,187 +37,104 @@ export default class Defence extends TranslatedComponent {
* @return {React.Component} contents
*/
render() {
const { opponent, sys, opponentWep } = this.props;
const { ship } = this.props;
const { language, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { shield, armour, shielddamage, armourdamage } = this.state;
const pd = opponent.standard[4].m;
const shields = ship.get(SHIELD_METRICS);
const shieldSourcesData = [];
const effectiveShieldData = [];
const shieldDamageTakenData = [];
const shieldSourcesTt = [];
const shieldDamageTakenAbsoluteTt = [];
const shieldDamageTakenExplosiveTt = [];
const shieldDamageTakenKineticTt = [];
const shieldDamageTakenThermalTt = [];
const effectiveShieldAbsoluteTt = [];
const effectiveShieldExplosiveTt = [];
const effectiveShieldKineticTt = [];
const effectiveShieldThermalTt = [];
let maxEffectiveShield = 0;
if (shield.total) {
shieldSourcesData.push({ value: Math.round(shield.generator), label: translate('generator') });
shieldSourcesData.push({ value: Math.round(shield.boosters), label: translate('boosters') });
shieldSourcesData.push({ value: Math.round(shield.cells), label: translate('cells') });
shieldSourcesData.push({ value: Math.round(shield.addition), label: translate('shield addition') });
// Data for pie chart (absolute MJ)
const shieldSourcesData = [
'byBoosters', 'byGenerator', 'byReinforcements', 'bySCBs',
].map((key) => { return { label: key, value: Math.round(shields[key]) }; });
if (shield.generator > 0) {
shieldSourcesTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
effectiveShieldAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
effectiveShieldExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
effectiveShieldKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
effectiveShieldThermalTt.push(<div key='generator'>{translate('generator') + ' ' + formats.int(shield.generator)}{units.MJ}</div>);
if (shield.boosters > 0) {
shieldSourcesTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
effectiveShieldAbsoluteTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
effectiveShieldExplosiveTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
effectiveShieldKineticTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
effectiveShieldThermalTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.int(shield.boosters)}{units.MJ}</div>);
}
// Data for tooltip
const shieldSourcesTt = shieldSourcesData.map((o) => {
let { label, value } = o;
return <div key={label}>
{translate(label)} {formats.int(value)}{units.MJ}
</div>;
});
if (shield.cells > 0) {
shieldSourcesTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
effectiveShieldAbsoluteTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
effectiveShieldExplosiveTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
effectiveShieldKineticTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
effectiveShieldThermalTt.push(<div key='cells'>{translate('cells') + ' ' + formats.int(shield.cells)}{units.MJ}</div>);
}
// Shield resistances
const shieldDamageTakenData = [
'absolute', 'explosive', 'kinetic', 'thermal',
].map((label) => {
const dmgMult = shields[label];
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
(label) => <div key={label}>
{translate(label)} {formats.pct1(dmgMult[label])}
</div>
);
return { label, value: Math.round(100 * dmgMult.withSys), tooltip };
});
// Add effective shield from resistances
const rawMj = shield.generator + shield.boosters + shield.cells;
const explosiveMj = rawMj / (shield.explosive.base) - rawMj;
if (explosiveMj != 0) effectiveShieldExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(explosiveMj)}{units.MJ}</div>);
const kineticMj = rawMj / (shield.kinetic.base) - rawMj;
if (kineticMj != 0) effectiveShieldKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(kineticMj)}{units.MJ}</div>);
const thermalMj = rawMj / (shield.thermal.base) - rawMj;
if (thermalMj != 0) effectiveShieldThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(thermalMj)}{units.MJ}</div>);
// Effective MJ
const effectiveShieldData = [
'absolute', 'explosive', 'kinetic', 'thermal'
].map((label) => {
const dmgMult = shields[label];
const raw = shields.withSCBs;
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
(label) => <div key={label}>
{translate(label)} {formats.int(raw * dmgMult[label])}{units.MJ}
</div>
);
return { label, value: Math.round(dmgMult.withSys * raw), tooltip };
});
const maxEffectiveShield = Math.max(...effectiveShieldData.map((o) => o.value));
// Add effective shield from power distributor SYS pips
if (shield.absolute.sys != 1) {
effectiveShieldAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.absolute.total - rawMj)}{units.MJ}</div>);
effectiveShieldExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.explosive.total - rawMj / shield.explosive.base)}{units.MJ}</div>);
effectiveShieldKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.kinetic.total - rawMj / shield.kinetic.base)}{units.MJ}</div>);
effectiveShieldThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.int(rawMj / shield.thermal.total - rawMj / shield.thermal.base)}{units.MJ}</div>);
}
}
const armour = ship.get(ARMOUR_METRICS);
const moduleProtection = ship.get(MODULE_PROTECTION_METRICS);
shieldDamageTakenAbsoluteTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.absolute.generator)}</div>);
shieldDamageTakenAbsoluteTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.absolute.boosters)}</div>);
shieldDamageTakenAbsoluteTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.absolute.sys)}</div>);
// Data for pie chart (absolute HP)
const armourSourcesData = ['base', 'byAlloys', 'byHRPs',].map(
(key) => { return { label: key, value: Math.round(armour[key]) }; }
);
shieldDamageTakenExplosiveTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.explosive.generator)}</div>);
shieldDamageTakenExplosiveTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.explosive.boosters)}</div>);
shieldDamageTakenExplosiveTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.explosive.sys)}</div>);
// Data for tooltip
const armourSourcesTt = armourSourcesData.map((o) => {
let { label, value } = o;
return <div key={label}>{translate(label)} {formats.int(value)}</div>;
});
shieldDamageTakenKineticTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.kinetic.generator)}</div>);
shieldDamageTakenKineticTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.kinetic.boosters)}</div>);
shieldDamageTakenKineticTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.kinetic.sys)}</div>);
// Armour resistances
const armourDamageTakenData = [
'absolute', 'explosive', 'kinetic', 'thermal', 'caustic',
].map((label) => {
const dmgMult = armour[label];
const tooltip = ['byAlloys', 'byHRPs'].map(
(label) => <div key={label}>
{translate(label)} {formats.pct1(dmgMult[label])}
</div>
);
return { label, value: Math.round(100 * dmgMult.damageMultiplier), tooltip };
});
shieldDamageTakenThermalTt.push(<div key='generator'>{translate('generator') + ' ' + formats.pct1(shield.thermal.generator)}</div>);
shieldDamageTakenThermalTt.push(<div key='boosters'>{translate('boosters') + ' ' + formats.pct1(shield.thermal.boosters)}</div>);
shieldDamageTakenThermalTt.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(shield.thermal.sys)}</div>);
const effectiveAbsoluteShield = shield.total / shield.absolute.total;
effectiveShieldData.push({ value: Math.round(effectiveAbsoluteShield), label: translate('absolute'), tooltip: effectiveShieldAbsoluteTt });
const effectiveExplosiveShield = shield.total / shield.explosive.total;
effectiveShieldData.push({ value: Math.round(effectiveExplosiveShield), label: translate('explosive'), tooltip: effectiveShieldExplosiveTt });
const effectiveKineticShield = shield.total / shield.kinetic.total;
effectiveShieldData.push({ value: Math.round(effectiveKineticShield), label: translate('kinetic'), tooltip: effectiveShieldKineticTt });
const effectiveThermalShield = shield.total / shield.thermal.total;
effectiveShieldData.push({ value: Math.round(effectiveThermalShield), label: translate('thermal'), tooltip: effectiveShieldThermalTt });
shieldDamageTakenData.push({ value: Math.round(shield.absolute.total * 100), label: translate('absolute'), tooltip: shieldDamageTakenAbsoluteTt });
shieldDamageTakenData.push({ value: Math.round(shield.explosive.total * 100), label: translate('explosive'), tooltip: shieldDamageTakenExplosiveTt });
shieldDamageTakenData.push({ value: Math.round(shield.kinetic.total * 100), label: translate('kinetic'), tooltip: shieldDamageTakenKineticTt });
shieldDamageTakenData.push({ value: Math.round(shield.thermal.total * 100), label: translate('thermal'), tooltip: shieldDamageTakenThermalTt });
maxEffectiveShield = Math.max(shield.total / shield.absolute.max, shield.total / shield.explosive.max, shield.total / shield.kinetic.max, shield.total / shield.thermal.max);
}
const armourSourcesData = [];
armourSourcesData.push({ value: Math.round(armour.bulkheads), label: translate('bulkheads') });
armourSourcesData.push({ value: Math.round(armour.reinforcement), label: translate('reinforcement') });
const armourSourcesTt = [];
const effectiveArmourAbsoluteTt = [];
const effectiveArmourExplosiveTt = [];
const effectiveArmourKineticTt = [];
const effectiveArmourThermalTt = [];
const effectiveArmourCausticTt = [];
if (armour.bulkheads > 0) {
armourSourcesTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourAbsoluteTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
effectiveArmourCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.int(armour.bulkheads)}</div>);
if (armour.reinforcement > 0) {
armourSourcesTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourAbsoluteTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
effectiveArmourCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.int(armour.reinforcement)}</div>);
}
}
const rawArmour = armour.bulkheads + armour.reinforcement;
const armourDamageTakenTt = [];
armourDamageTakenTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.absolute.bulkheads)}</div>);
armourDamageTakenTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.absolute.reinforcement)}</div>);
const armourDamageTakenExplosiveTt = [];
armourDamageTakenExplosiveTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.explosive.bulkheads)}</div>);
armourDamageTakenExplosiveTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.explosive.reinforcement)}</div>);
if (armour.explosive.total != 1) effectiveArmourExplosiveTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.explosive.total - rawArmour)}</div>);
const armourDamageTakenKineticTt = [];
armourDamageTakenKineticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.kinetic.bulkheads)}</div>);
armourDamageTakenKineticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.kinetic.reinforcement)}</div>);
if (armour.kinetic.total != 1) effectiveArmourKineticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.kinetic.total - rawArmour)}</div>);
const armourDamageTakenThermalTt = [];
armourDamageTakenThermalTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.thermal.bulkheads)}</div>);
armourDamageTakenThermalTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.thermal.reinforcement)}</div>);
if (armour.thermal.total != 1) effectiveArmourThermalTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.thermal.total - rawArmour)}</div>);
const armourDamageTakenCausticTt = [];
armourDamageTakenCausticTt.push(<div key='bulkheads'>{translate('bulkheads') + ' ' + formats.pct1(armour.caustic.bulkheads)}</div>);
armourDamageTakenCausticTt.push(<div key='reinforcement'>{translate('reinforcement') + ' ' + formats.pct1(armour.caustic.reinforcement)}</div>);
if (armour.thermal.total != 1) effectiveArmourCausticTt.push(<div key='resistance'>{translate('resistance') + ' ' + formats.int(rawArmour / armour.caustic.total - rawArmour)}</div>);
const effectiveArmourData = [];
const effectiveAbsoluteArmour = armour.total / armour.absolute.total;
effectiveArmourData.push({ value: Math.round(effectiveAbsoluteArmour), label: translate('absolute'), tooltip: effectiveArmourAbsoluteTt });
const effectiveExplosiveArmour = armour.total / armour.explosive.total;
effectiveArmourData.push({ value: Math.round(effectiveExplosiveArmour), label: translate('explosive'), tooltip: effectiveArmourExplosiveTt });
const effectiveKineticArmour = armour.total / armour.kinetic.total;
effectiveArmourData.push({ value: Math.round(effectiveKineticArmour), label: translate('kinetic'), tooltip: effectiveArmourKineticTt });
const effectiveThermalArmour = armour.total / armour.thermal.total;
effectiveArmourData.push({ value: Math.round(effectiveThermalArmour), label: translate('thermal'), tooltip: effectiveArmourThermalTt });
const effectiveCausticArmour = armour.total / armour.caustic.total;
effectiveArmourData.push({ value: Math.round(effectiveCausticArmour), label: translate('caustic'), tooltip: effectiveArmourCausticTt });
const armourDamageTakenData = [];
armourDamageTakenData.push({ value: Math.round(armour.absolute.total * 100), label: translate('absolute'), tooltip: armourDamageTakenTt });
armourDamageTakenData.push({ value: Math.round(armour.explosive.total * 100), label: translate('explosive'), tooltip: armourDamageTakenExplosiveTt });
armourDamageTakenData.push({ value: Math.round(armour.kinetic.total * 100), label: translate('kinetic'), tooltip: armourDamageTakenKineticTt });
armourDamageTakenData.push({ value: Math.round(armour.thermal.total * 100), label: translate('thermal'), tooltip: armourDamageTakenThermalTt });
armourDamageTakenData.push({ value: Math.round(armour.caustic.total * 100), label: translate('caustic'), tooltip: armourDamageTakenCausticTt });
// Effective HP
const effectiveArmourData = [
'absolute', 'explosive', 'kinetic', 'thermal'
].map((label) => {
const dmgMult = armour[label];
const raw = armour.armour;
const tooltip = ['byBoosters', 'byGenerator', 'bySys'].map(
(label) => <div key={label}>
{translate(label)} {formats.int(raw * dmgMult[label])}
</div>
);
return { label, value: Math.round(dmgMult.damageMultiplier * raw), tooltip };
});
return (
<span id='defence'>
{shield.total ? <span>
{shields.withSCBs ? <span>
<div className='group quarter'>
<h2>{translate('shield metrics')}</h2>
<br/>
<h2 onMouseOver={termtip.bind(null, <div>{shieldSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shield.total)}{units.MJ}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}<br/>{shielddamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(shield.total, shielddamage.totalsdps, shielddamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECOVER'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}<br/>{shield.recover === Math.Inf ? translate('never') : formats.time(shield.recover)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECHARGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}<br/>{shield.recharge === Math.Inf ? translate('never') : formats.time(shield.recharge)}</h2>
<h2 onMouseOver={termtip.bind(null, <div>{shieldSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw shield strength')}<br/>{formats.int(shields.withSCBs)}{units.MJ}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_SHIELDS')}<br/>TODO</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECOVER'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECOVER_SHIELDS')}<br/>{shields.recover ? formats.time(shields.recover) : translate('never')}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SG_RECHARGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_RECHARGE_SHIELDS')}<br/>{shields.recharge ? formats.time(shields.recharge) : translate('never')}</h2>
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_SOURCES'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield sources')}</h2>
@@ -250,11 +152,11 @@ export default class Defence extends TranslatedComponent {
<div className='group quarter'>
<h2>{translate('armour metrics')}</h2>
<h2 onMouseOver={termtip.bind(null, <div>{armourSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}<br/>{formats.int(armour.total)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}<br/>{armourdamage.totalsdps == 0 ? translate('ever') : formats.time(Calc.timeToDeplete(armour.total, armourdamage.totalsdps, armourdamage.totalseps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * opponentWep / 4))}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('raw module armour')}<br/>{formats.int(armour.modulearmour)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_EXTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}<br/>{formats.pct1(armour.moduleprotection / 2)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_INTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}<br/>{formats.pct1(armour.moduleprotection)}</h2>
<h2 onMouseOver={termtip.bind(null, <div>{armourSourcesTt}</div>)} onMouseOut={tooltip.bind(null, null)} className='summary'>{translate('raw armour strength')}<br/>{formats.int(armour.armour)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_LOSE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_LOSE_ARMOUR')}<br/>TODO</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('raw module armour')}<br/>{formats.int(moduleProtection.moduleArmour)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_EXTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_EXTERNAL')}<br/>{formats.pct1((1 - moduleProtection.moduleProtection) / 2)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_MODULE_PROTECTION_INTERNAL'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_MODULE_PROTECTION_INTERNAL')}<br/>{formats.pct1(1 - moduleProtection.moduleProtection)}</h2>
<br/>
</div>
<div className='group quarter'>

View File

@@ -2,98 +2,58 @@ import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import LineChart from '../components/LineChart';
import * as Calc from '../shipyard/Calculations';
import { getBoostMultiplier, getSpeedMultipliers } from 'ed-forge/lib/stats/SpeedProfile';
import { ShipProps } from 'ed-forge';
const { LADEN_MASS } = ShipProps;
/**
* Engine profile for a given ship
*/
export default class EngineProfile extends TranslatedComponent {
static propTypes = {
code: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
cargo: PropTypes.number.isRequired,
fuel: PropTypes.number.isRequired,
eng: PropTypes.number.isRequired,
pips: PropTypes.number.isRequired,
boost: PropTypes.bool.isRequired,
marker: PropTypes.string.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
const ship = this.props.ship;
this.state = {
calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, ship, this.props.eng, this.props.boost)
};
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.marker != this.props.marker) {
this.setState({ calcMaxSpeedFunc: this.calcMaxSpeed.bind(this, nextProps.ship, nextProps.eng, nextProps.boost) });
}
return true;
}
/**
* Calculate the top speed for this ship given thrusters, mass and pips to ENG
* @param {Object} ship The ship
* @param {Object} eng The number of pips to ENG
* @param {Object} boost If boost is enabled
* @param {Object} mass The mass at which to calculate the top speed
* @return {number} The maximum speed
*/
calcMaxSpeed(ship, eng, boost, mass) {
// Obtain the top speed
return Calc.calcSpeed(mass, ship.speed, ship.standard[1].m, ship.pipSpeed, eng, ship.boost / ship.speed, boost);
}
/**
* Render engine profile
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { ship, cargo, eng, fuel, boost } = this.props;
const { language } = this.context;
const { translate } = language;
const { code, ship, pips, boost } = this.props;
// Calculate bounds for our line chart
const thrusters = ship.standard[1].m;
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
const maxMass = thrusters.getMaxMass();
const mass = ship.unladenMass + fuel + cargo;
const minSpeed = Calc.calcSpeed(maxMass, ship.speed, thrusters, ship.pipSpeed, 0, ship.boost / ship.speed, false);
const maxSpeed = Calc.calcSpeed(minMass, ship.speed, thrusters, ship.pipSpeed, 4, ship.boost / ship.speed, true);
// Add a mark at our current mass
const mark = Math.min(mass, maxMass);
const code = `${ship.toString()}:${cargo}:${fuel}:${eng}:${boost}`;
const minMass = ship.readProp('hullmass');
const maxMass = ship.getThrusters().get('enginemaximalmass');
const baseSpeed = ship.readProp('speed');
const baseBoost = getBoostMultiplier(ship);
const cb = (eng, boost, mass) => {
const mult = getSpeedMultipliers(ship, mass)[(boost ? 4 : eng) / 0.5];
return baseSpeed * (boost ? baseBoost : 1) * mult;
};
// This graph can have a precipitous fall-off so we use lots of points to make it look a little smoother
return (
<LineChart
xMin={minMass}
xMax={maxMass}
yMin={minSpeed}
yMax={maxSpeed}
xMark={mark}
yMin={cb(0, false, maxMass)}
yMax={cb(4, true, minMass)}
// Add a mark at our current mass
xMark={Math.min(ship.get(LADEN_MASS), maxMass)}
xLabel={translate('mass')}
xUnit={translate('T')}
yLabel={translate('maximum speed')}
yUnit={translate('m/s')}
func={this.state.calcMaxSpeedFunc}
func={cb.bind(this, pips.Eng.base + pips.Eng.mc, boost)}
points={1000}
code={code}
// Encode boost in code to re-render on state change
code={`${pips.Eng.base + pips.Eng.mc}:${Number(boost)}:${code}`}
aspect={0.7}
/>
);

View File

@@ -3,46 +3,21 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import LineChart from '../components/LineChart';
import * as Calc from '../shipyard/Calculations';
import { calculateJumpRange } from 'ed-forge/lib/stats/JumpRangeProfile';
import { ShipProps } from 'ed-forge';
const { LADEN_MASS } = ShipProps;
/**
* FSD profile for a given ship
*/
export default class FSDProfile extends TranslatedComponent {
static propTypes = {
code: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
cargo: PropTypes.number.isRequired,
fuel: PropTypes.number.isRequired,
marker: PropTypes.string.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
const ship = this.props.ship;
this.state = {
calcMaxRangeFunc: this._calcMaxRange.bind(this, ship, this.props.fuel)
};
}
/**
* Update the state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.marker != this.props.marker) {
this.setState({ calcMaxRangeFunc: this._calcMaxRange.bind(this, nextProps.ship, nextProps.fuel) });
}
return true;
}
/**
* Calculate the maximum range for this ship across its applicable mass
* @param {Object} ship The ship
@@ -60,36 +35,27 @@ export default class FSDProfile extends TranslatedComponent {
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { ship, cargo, fuel } = this.props;
// Calculate bounds for our line chart - use thruster info for X
const thrusters = ship.standard[1].m;
const fsd = ship.standard[2].m;
const minMass = ship.calcLowestPossibleMass({ th: thrusters });
const maxMass = thrusters.getMaxMass();
const mass = ship.unladenMass + fuel + cargo;
const minRange = 0;
const maxRange = Calc.jumpRange(minMass + fsd.getMaxFuelPerJump(), fsd, fsd.getMaxFuelPerJump(), ship);
// Add a mark at our current mass
const mark = Math.min(mass, maxMass);
const code = ship.name + ship.toString() + '.' + fuel;
const { language } = this.context;
const { translate } = language;
const { code, ship } = this.props;
const minMass = ship.readProp('hullmass');
const maxMass = ship.getThrusters().get('enginemaximalmass');
const mass = ship.get(LADEN_MASS);
const cb = (mass) => calculateJumpRange(ship, mass, Infinity, true);
return (
<LineChart
xMin={minMass}
xMax={maxMass}
yMin={minRange}
yMax={maxRange}
xMark={mark}
yMin={0}
yMax={cb(minMass)}
// Add a mark at our current mass
xMark={Math.min(mass, maxMass)}
xLabel={translate('mass')}
xUnit={translate('T')}
yLabel={translate('maximum range')}
yUnit={translate('LY')}
func={this.state.calcMaxRangeFunc}
func={cb}
points={200}
code={code}
aspect={0.7}

View File

@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import Slider from '../components/Slider';
import autoBind from 'auto-bind';
/**
* Fuel slider
@@ -21,8 +22,7 @@ export default class Fuel extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
this._fuelChange = this._fuelChange.bind(this);
autoBind(this);
}
/**

View File

@@ -1,151 +0,0 @@
import React from 'react';
import cn from 'classnames';
import Slot from './Slot';
import Persist from '../stores/Persist';
import {
DamageAbsolute,
DamageKinetic,
DamageThermal,
DamageExplosive,
MountFixed,
MountGimballed,
MountTurret,
ListModifications,
Modified
} from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/**
* Hardpoint / Utility Slot
*/
export default class HardpointSlot extends Slot {
/**
* Get the CSS class name for the slot.
* @return {string} CSS Class name
*/
_getClassNames() {
return this.props.maxClass > 0 ? 'hardpoint' : null;
}
/**
* Get the label for the slot
* @param {Function} translate Translate function
* @return {string} Label
*/
_getMaxClassLabel(translate) {
return translate(['U', 'S', 'M', 'L', 'H'][this.props.maxClass]);
}
/**
* Generate the slot contents
* @param {Object} m Mounted Module
* @param {Boolean} enabled Slot enabled
* @param {Function} translate Translate function
* @param {Object} formats Localized Formats map
* @param {Object} u Localized Units Map
* @return {React.Component} Slot contents
*/
_getSlotDetails(m, enabled, translate, formats, u) {
if (m) {
let classRating = `${m.class}${m.rating}${m.missile ? '/' + m.missile : ''}`;
let { drag, drop } = this.props;
let { termtip, tooltip } = this.context;
let validMods = Modifications.modules[m.grp].modifications || [];
let showModuleResistances = Persist.showModuleResistances();
// Modifications tooltip shows blueprint and grade, if available
let modTT = translate('modified');
if (m && m.blueprint && m.blueprint.name) {
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id >= 0) {
modTT += ', ' + translate(m.blueprint.special.name);
}
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
</div>
);
}
const className = cn('details', enabled ? '' : 'disabled');
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
<div className={'cb'}>
<div className={'l'}>
{m.mount && m.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')}
onMouseOut={tooltip.bind(null, null)}><MountFixed/></span> : ''}
{m.mount && m.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')}
onMouseOut={tooltip.bind(null, null)}><MountGimballed/></span> : ''}
{m.mount && m.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')}
onMouseOut={tooltip.bind(null, null)}><MountTurret/></span> : ''}
{m.getDamageDist() && m.getDamageDist().K ? <span onMouseOver={termtip.bind(null, 'kinetic')}
onMouseOut={tooltip.bind(null, null)}><DamageKinetic/></span> : ''}
{m.getDamageDist() && m.getDamageDist().T ? <span onMouseOver={termtip.bind(null, 'thermal')}
onMouseOut={tooltip.bind(null, null)}><DamageThermal/></span> : ''}
{m.getDamageDist() && m.getDamageDist().E ? <span onMouseOver={termtip.bind(null, 'explosive')}
onMouseOut={tooltip.bind(null, null)}><DamageExplosive/></span> : ''}
{m.getDamageDist() && m.getDamageDist().A ? <span onMouseOver={termtip.bind(null, 'absolute')}
onMouseOut={tooltip.bind(null, null)}><DamageAbsolute/></span> : ''}
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r'
onMouseOver={termtip.bind(null, modTT)}
onMouseOut={tooltip.bind(null, null)}><Modified/></span> : null}
</div>
<div className={'r'}>{formats.round(m.getMass())}{u.T}</div>
</div>
<div className={'cb'}>
{m.getDps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'dpssdps' : 'dps')}
onMouseOut={tooltip.bind(null, null)}>{translate('DPS')}: {formats.round1(m.getDps())} {m.getClip() ?
<span>({formats.round1(m.getSDps())})</span> : null}</div> : null}
{m.getDamage() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getDamage() ? 'shotdmg' : 'shotdmg')}
onMouseOut={tooltip.bind(null, null)}>{translate('shotdmg')}: {formats.round1(m.getDamage())}</div> : null}
{m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'epsseps' : 'eps')}
onMouseOut={tooltip.bind(null, null)}>{translate('EPS')}: {formats.round1(m.getEps())}{u.MW} {m.getClip() ?
<span>({formats.round1(m.getEps() * m.getSustainedFactor())}{u.MW})</span> : null}</div> : null}
{m.getHps() ? <div className={'l'} onMouseOver={termtip.bind(null, m.getClip() ? 'hpsshps' : 'hps')}
onMouseOut={tooltip.bind(null, null)}>{translate('HPS')}: {formats.round1(m.getHps())} {m.getClip() ?
<span>({formats.round1(m.getHps() * m.getSustainedFactor())})</span> : null}</div> : null}
{m.getDps() && m.getEps() ? <div className={'l'} onMouseOver={termtip.bind(null, 'dpe')}
onMouseOut={tooltip.bind(null, null)}>{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}</div> : null}
{m.getRoF() ? <div className={'l'} onMouseOver={termtip.bind(null, 'rof')}
onMouseOut={tooltip.bind(null, null)}>{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}</div> : null}
{m.getRange() ? <div
className={'l'}>{translate('range', m.grp)} {formats.f1(m.getRange() / 1000)}{u.km}</div> : null}
{m.getScanTime() ? <div
className={'l'}>{translate('scantime')} {formats.f1(m.getScanTime())}{u.s}</div> : null}
{m.getFalloff() ? <div
className={'l'}>{translate('falloff')} {formats.round(m.getFalloff() / 1000)}{u.km}</div> : null}
{m.getShieldBoost() ? <div className={'l'}>+{formats.pct1(m.getShieldBoost())}</div> : null}
{m.getAmmo() ? <div
className={'l'}>{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}</div> : null}
{m.getReload() ? <div className={'l'}>{translate('wep_reload')}: {formats.round(m.getReload())}{u.s}</div> : null}
{m.getShotSpeed() ? <div
className={'l'}>{translate('shotspeed')}: {formats.int(m.getShotSpeed())}{u.mps}</div> : null}
{m.getPiercing() ? <div className={'l'}>{translate('piercing')}: {formats.int(m.getPiercing())}</div> : null}
{m.getJitter() ? <div className={'l'}>{translate('jitter')}: {formats.f2(m.getJitter())}°</div> : null}
{m.getScanAngle() ? <div className={'l'}>{translate('scan angle')}: {formats.f2(m.getScanAngle())}°</div> : null}
{m.getScanRange() ? <div className={'l'}>{translate('scan range')}: {formats.int(m.getScanRange())}{u.m}</div> : null}
{m.getMaxAngle() ? <div className={'l'}>{translate('max angle')}: {formats.f2(m.getMaxAngle())}°</div> : null}
{showModuleResistances && m.getExplosiveResistance() ? <div
className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null}
{showModuleResistances && m.getKineticResistance() ? <div
className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null}
{showModuleResistances && m.getThermalResistance() ? <div
className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null}
{m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null}
{m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={modButton => this.modButton = modButton}>
<button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation}
onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}>
<ListModifications/></button>
</div> : null}
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}
}
}

View File

@@ -1,42 +1,29 @@
import React from 'react';
import SlotSection from './SlotSection';
import HardpointSlot from './HardpointSlot';
import Slot from './Slot';
import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import autoBind from 'auto-bind';
/**
* Hardpoint slot section
*/
export default class HardpointSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context, 'hardpoints', 'hardpoints');
this._empty = this._empty.bind(this);
this.selectedRefId = null;
this.firstRefId = 'emptyall';
this.lastRefId = 'nl-F';
}
/**
* Handle focus when component updates
* @param {Object} prevProps React Component properties
*/
componentDidUpdate(prevProps) {
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
constructor(props) {
super(props, 'hardpoints');
autoBind(this);
}
/**
* Empty all slots
*/
_empty() {
this.selectedRefId = 'emptyall';
this.props.ship.emptyWeapons();
this.props.onChange();
// TODO:
// this.props.ship.emptyWeapons();
this._close();
}
@@ -47,9 +34,8 @@ export default class HardpointSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fill(group, mount, event) {
this.selectedRefId = group + '-' + mount;
this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
this.props.onChange();
// TODO:
// this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt'));
this._close();
}
@@ -65,33 +51,25 @@ export default class HardpointSlotSection extends SlotSection {
* @return {Array} Array of Slots
*/
_getSlots() {
let { ship, currentMenu } = this.props;
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
let { originSlot, targetSlot } = this.state;
let slots = [];
let hardpoints = ship.hardpoints;
let availableModules = ship.getAvailableModules();
for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i];
if (h.maxClass) {
slots.push(<HardpointSlot
key={i}
maxClass={h.maxClass}
availableModules={() => availableModules.getHps(h.maxClass)}
onOpen={this._openMenu.bind(this, h)}
onSelect={this._selectModule.bind(this, h)}
onChange={this.props.onChange}
selected={currentMenu == h}
for (let h of ship.getHardpoints(undefined, true)) {
slots.push(<Slot
key={h.object.Slot}
maxClass={h.getSize()}
currentMenu={currentMenu}
drag={this._drag.bind(this, h)}
dragOver={this._dragOverSlot.bind(this, h)}
drop={this._drop}
dropClass={this._dropClass(h, originSlot, targetSlot)}
ship={ship}
m={h.m}
m={h}
enabled={h.enabled ? true : false}
propsToShow={propsToShow}
onPropToggle={onPropToggle}
/>);
}
}
return slots;
}
@@ -101,68 +79,68 @@ export default class HardpointSlotSection extends SlotSection {
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) {
_getSectionMenu() {
const { translate } = this.context.language;
let _fill = this._fill;
return <div className='select hardpoint' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
<ul>
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
<li className='lc' tabIndex="0" onClick={this._empty} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
</ul>
<div className='select-group cap'>{translate('pl')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'pl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pl-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'pl', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('ul')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'ul', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ul-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'ul', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('bl')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'bl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['bl-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'bl', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('mc')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'mc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mc-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'mc', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('c')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'c', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['c-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'c', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('fc')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'G')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-G'] = smRef}><MountGimballed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'fc', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['fc-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'G')}><MountGimballed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'fc', 'T')}><MountTurret className='lg'/></li>
</ul>
<div className='select-group cap'>{translate('pa')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'pa', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pa-F'] = smRef}>{translate('pa')}</li>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'pa', 'F')}>{translate('pa')}</li>
</ul>
<div className='select-group cap'>{translate('rg')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'rg', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rg-F'] = smRef}>{translate('rg')}</li>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'rg', 'F')}>{translate('rg')}</li>
</ul>
<div className='select-group cap'>{translate('nl')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_fill.bind(this, 'nl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['nl-F'] = smRef}>{translate('nl')}</li>
<li className='lc' tabIndex="0" onClick={_fill.bind(this, 'nl', 'F')}>{translate('nl')}</li>
</ul>
<div className='select-group cap'>{translate('rfl')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'F')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-F'] = smRef}><MountFixed className='lg'/></li>
<li className='c' tabIndex='0' onClick={_fill.bind(this, 'rfl', 'T')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['rfl-T'] = smRef}><MountTurret className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'rfl', 'F')}><MountFixed className='lg'/></li>
<li className="c" tabIndex="0" onClick={_fill.bind(this, 'rfl', 'T')}><MountTurret className='lg'/></li>
</ul>
</div>;
}
}

View File

@@ -56,7 +56,6 @@ function selectAll(e) {
* Coriolis App Header section / menus
*/
export default class Header extends TranslatedComponent {
/**
* Constructor
* @param {Object} props React Component properties
@@ -608,7 +607,7 @@ export default class Header extends TranslatedComponent {
</div>
<div className='l menu'>
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0})} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
<div className={cn('menu-header', { selected: openedMenu == 'announce', disabled: this.props.announcements.length === 0 })} onClick={this.props.announcements.length !== 0 && this._openAnnounce}>
<span className='menu-item-label'>{translate('announcements')}</span>
</div>
{openedMenu == 'announce' ? this._getAnnouncementsMenu() : null}
@@ -639,5 +638,4 @@ export default class Header extends TranslatedComponent {
</header>
);
}
}

View File

@@ -1,98 +0,0 @@
import React from 'react';
import cn from 'classnames';
import Slot from './Slot';
import Persist from '../stores/Persist';
import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/**
* Internal Slot
*/
export default class InternalSlot extends Slot {
/**
* Generate the slot contents
* @param {Object} m Mounted Module
* @param {Boolean} enabled Slot enabled
* @param {Function} translate Translate function
* @param {Object} formats Localized Formats map
* @param {Object} u Localized Units Map
* @return {React.Component} Slot contents
*/
_getSlotDetails(m, enabled, translate, formats, u) {
if (m) {
let classRating = m.class + m.rating;
let { drag, drop, ship } = this.props;
let { termtip, tooltip } = this.context;
let validMods = (Modifications.modules[m.grp] ? Modifications.modules[m.grp].modifications : []);
let showModuleResistances = Persist.showModuleResistances();
// Modifications tooltip shows blueprint and grade, if available
let modTT = translate('modified');
if (m && m.blueprint && m.blueprint.name) {
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id >= 0) {
modTT += ', ' + translate(m.blueprint.special.name);
}
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
</div>
);
}
let mass = m.getMass() || m.cargo || m.fuel || 0;
const className = cn('details', enabled ? '' : 'disabled');
return <div className={className} draggable='true' onDragStart={drag} onDragEnd={drop}>
<div className={'cb'}>
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : ''}</div>
<div className={'r'}>{formats.round(mass)}{u.T}</div>
</div>
<div className={'cb'}>
{ m.getOptMass() ? <div className={'l'}>{translate('optmass', 'sg')}: {formats.int(m.getOptMass())}{u.T}</div> : null }
{ m.getMaxMass() ? <div className={'l'}>{translate('maxmass', 'sg')}: {formats.int(m.getMaxMass())}{u.T}</div> : null }
{ m.bins ? <div className={'l'}>{m.bins} <u>{translate('bins')}</u></div> : null }
{ m.bays ? <div className={'l'}>{translate('bays')}: {m.bays}</div> : null }
{ m.rebuildsperbay ? <div className={'l'}>{translate('rebuildsperbay')}: {m.rebuildsperbay}</div> : null }
{ m.rate ? <div className={'l'}>{translate('rate')}: {m.rate}{u.kgs}&nbsp;&nbsp;&nbsp;{translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}</div> : null }
{ m.getAmmo() && m.grp !== 'scb' ? <div className={'l'}>{translate('ammunition')}: {formats.gen(m.getAmmo())}</div> : null }
{ m.getSpinup() ? <div className={'l'}>{translate('spinup')}: {formats.f1(m.getSpinup())}{u.s}</div> : null }
{ m.getDuration() ? <div className={'l'}>{translate('duration')}: {formats.f1(m.getDuration())}{u.s}</div> : null }
{ m.grp === 'scb' ? <div className={'l'}>{translate('cells')}: {formats.int(m.getAmmo() + 1)}</div> : null }
{ m.grp === 'gsrp' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
{ m.grp === 'gfsb' ? <div className={'l'}>{translate('jump addition')}: {formats.f1(m.getJumpBoost())}{u.LY}</div> : null }
{ m.grp === 'gs' ? <div className={'l'}>{translate('shield addition')}: {formats.f1(m.getShieldAddition())}{u.MJ}</div> : null }
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('shieldreinforcement')}: {formats.f1(m.getDuration() * m.getShieldReinforcement())}{u.MJ}</div> : null }
{ m.getShieldReinforcement() ? <div className={'l'}>{translate('total')}: {formats.int((m.getAmmo() + 1) * (m.getDuration() * m.getShieldReinforcement()))}{u.MJ}</div> : null }
{ m.repair ? <div className={'l'}>{translate('repair')}: {m.repair}</div> : null }
{ m.getFacingLimit() ? <div className={'l'}>{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°</div> : null }
{ m.getRange() ? <div className={'l'}>{translate('range')} {formats.f2(m.getRange())}{u.km}</div> : null }
{ m.getRangeT() ? <div className={'l'}>{translate('ranget')} {formats.f1(m.getRangeT())}{u.s}</div> : null }
{ m.getTime() ? <div className={'l'}>{translate('time')}: {formats.time(m.getTime())}</div> : null }
{ m.getHackTime() ? <div className={'l'}>{translate('hacktime')}: {formats.time(m.getHackTime())}</div> : null }
{ m.maximum ? <div className={'l'}>{translate('max')}: {(m.maximum)}</div> : null }
{ m.rangeLS ? <div className={'l'}>{translate('range')}: {m.rangeLS}{u.Ls}</div> : null }
{ m.rangeLS === null ? <div className={'l'}>{u.Ls}</div> : null }
{ m.rangeRating ? <div className={'l'}>{translate('range')}: {m.rangeRating}</div> : null }
{ m.passengers ? <div className={'l'}>{translate('passengers')}: {m.passengers}</div> : null }
{ m.getRegenerationRate() ? <div className='l'>{translate('regen')}: {formats.round1(m.getRegenerationRate())}{u.ps}</div> : null }
{ m.getBrokenRegenerationRate() ? <div className='l'>{translate('brokenregen')}: {formats.round1(m.getBrokenRegenerationRate())}{u.ps}</div> : null }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ showModuleResistances && m.getCausticResistance() ? <div className='l'>{translate('causres')}: {formats.pct(m.getCausticResistance())}</div> : null }
{ m.getHullReinforcement() ? <div className='l'>{translate('armour')}: {formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)}</div> : null }
{ m.getProtection() ? <div className='l'>{translate('protection')}: {formats.rPct(m.getProtection())}</div> : null }
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
{ m && validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>;
} else {
return <div className={'empty'}>{translate('empty')}</div>;
}
}
}

View File

@@ -1,52 +1,30 @@
import React from 'react';
import SlotSection from './SlotSection';
import InternalSlot from './InternalSlot';
import Slot from './Slot';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { canMount } from '../utils/SlotFunctions';
import autoBind from 'auto-bind';
/**
* Internal slot section
*/
export default class InternalSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context, 'internal', 'optional internal');
this._empty = this._empty.bind(this);
this._fillWithCargo = this._fillWithCargo.bind(this);
this._fillWithCells = this._fillWithCells.bind(this);
this._fillWithArmor = this._fillWithArmor.bind(this);
this._fillWithModuleReinforcementPackages = this._fillWithModuleReinforcementPackages.bind(this);
this._fillWithFuelTanks = this._fillWithFuelTanks.bind(this);
this._fillWithLuxuryCabins = this._fillWithLuxuryCabins.bind(this);
this._fillWithFirstClassCabins = this._fillWithFirstClassCabins.bind(this);
this._fillWithBusinessClassCabins = this._fillWithBusinessClassCabins.bind(this);
this._fillWithEconomyClassCabins = this._fillWithEconomyClassCabins.bind(this);
this.selectedRefId = null;
this.firstRefId = 'emptyall';
this.lastRefId = this.sectionRefArr['pcq'] ? 'pcq' : 'pcm';
}
/**
* Handle focus when component updates
* @param {Object} prevProps React Component properties
*/
componentDidUpdate(prevProps) {
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
constructor(props) {
super(props, 'optional internal');
autoBind(this);
}
/**
* Empty all slots
*/
_empty() {
this.selectedRefId = 'emptyall';
this.props.ship.emptyInternal();
this.props.onChange();
// TODO:
// this.props.ship.emptyInternal();
this._close();
}
@@ -55,7 +33,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithCargo(event) {
this.selectedRefId = 'cargo';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -63,7 +40,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E'));
}
});
this.props.onChange();
this._close();
}
@@ -72,7 +48,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithFuelTanks(event) {
this.selectedRefId = 'ft';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -80,7 +55,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('ft', slot.maxClass, 'C'));
}
});
this.props.onChange();
this._close();
}
@@ -89,7 +63,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithLuxuryCabins(event) {
this.selectedRefId = 'pcq';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -97,7 +70,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pcq', Math.min(slot.maxClass, 6), 'B')); // Passenger cabins top out at 6
}
});
this.props.onChange();
this._close();
}
@@ -106,7 +78,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithFirstClassCabins(event) {
this.selectedRefId = 'pcm';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -114,7 +85,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pcm', Math.min(slot.maxClass, 6), 'C')); // Passenger cabins top out at 6
}
});
this.props.onChange();
this._close();
}
@@ -123,7 +93,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithBusinessClassCabins(event) {
this.selectedRefId = 'pci';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -131,7 +100,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pci', Math.min(slot.maxClass, 6), 'D')); // Passenger cabins top out at 6
}
});
this.props.onChange();
this._close();
}
@@ -140,7 +108,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithEconomyClassCabins(event) {
this.selectedRefId = 'pce';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -148,7 +115,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('pce', Math.min(slot.maxClass, 6), 'E')); // Passenger cabins top out at 6
}
});
this.props.onChange();
this._close();
}
@@ -157,7 +123,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithCells(event) {
this.selectedRefId = 'scb';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
let chargeCap = 0; // Capacity of single activation
@@ -168,7 +133,6 @@ export default class InternalSlotSection extends SlotSection {
chargeCap += slot.m.recharge;
}
});
this.props.onChange();
this._close();
}
@@ -177,7 +141,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithArmor(event) {
this.selectedRefId = 'hr';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -185,7 +148,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D
}
});
this.props.onChange();
this._close();
}
@@ -194,7 +156,6 @@ export default class InternalSlotSection extends SlotSection {
* @param {SyntheticEvent} event Event
*/
_fillWithModuleReinforcementPackages(event) {
this.selectedRefId = 'mrp';
let clobber = event.getModifierState('Alt');
let ship = this.props.ship;
ship.internal.forEach((slot) => {
@@ -202,7 +163,6 @@ export default class InternalSlotSection extends SlotSection {
ship.use(slot, ModuleUtils.findInternal('mrp', Math.min(slot.maxClass, 5), 'D')); // Module reinforcements top out at 5D
}
});
this.props.onChange();
this._close();
}
@@ -219,31 +179,20 @@ export default class InternalSlotSection extends SlotSection {
*/
_getSlots() {
let slots = [];
let { currentMenu, ship } = this.props;
let { currentMenu, ship, propsToShow, onPropToggle } = this.props;
let { originSlot, targetSlot } = this.state;
let { internal, fuelCapacity } = ship;
let availableModules = ship.getAvailableModules();
for (let i = 0, l = internal.length; i < l; i++) {
let s = internal[i];
slots.push(<InternalSlot
key={i}
maxClass={s.maxClass}
availableModules={() => availableModules.getInts(ship, s.maxClass, s.eligible)}
onOpen={this._openMenu.bind(this,s)}
onChange={this.props.onChange}
onSelect={this._selectModule.bind(this, s)}
selected={currentMenu == s}
eligible={s.eligible}
m={s.m}
drag={this._drag.bind(this, s)}
dragOver={this._dragOverSlot.bind(this, s)}
for (const m of ship.getInternals(undefined, true)) {
slots.push(<Slot
key={m.object.Slot}
currentMenu={currentMenu}
m={m}
drag={this._drag.bind(this, m)}
dragOver={this._dragOverSlot.bind(this, m)}
drop={this._drop}
dropClass={this._dropClass(s, originSlot, targetSlot)}
fuel={fuelCapacity}
ship={ship}
enabled={s.enabled ? true : false}
dropClass={this._dropClass(m, originSlot, targetSlot)}
propsToShow={propsToShow}
onPropToggle={onPropToggle}
/>);
}
@@ -256,22 +205,23 @@ export default class InternalSlotSection extends SlotSection {
* @param {Function} ship The ship
* @return {React.Component} Section menu
*/
_getSectionMenu(translate, ship) {
_getSectionMenu() {
const { ship } = this.props;
const { translate } = this.context.language;
return <div className='select' onClick={e => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
<ul>
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCargo} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['cargo'] = smRef}>{translate('cargo')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCells} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['scb'] = smRef}>{translate('scb')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithArmor} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hr'] = smRef}>{translate('hr')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['mrp'] = smRef}>{translate('mrp')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ft'] = smRef}>{translate('ft')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pce'] = smRef}>{translate('pce')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pci'] = smRef}>{translate('pci')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown} ref={smRef => this.sectionRefArr['pcm'] = smRef}>{translate('pcm')}</li>
{ ship.luxuryCabins ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['pcq'] = smRef}>{translate('pcq')}</li> : ''}
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCargo}>{translate('cargo')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithCells}>{translate('scb')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithArmor}>{translate('hr')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithModuleReinforcementPackages}>{translate('mrp')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFuelTanks}>{translate('ft')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithEconomyClassCabins}>{translate('pce')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithBusinessClassCabins}>{translate('pci')}</li>
<li className='lc' tabIndex='0' onClick={this._fillWithFirstClassCabins} onKeyDown={ship.luxuryCabins ? '' : this._keyDown}>{translate('pcm')}</li>
{ ship.luxuryCabins ? <li className='lc' tabIndex='0' onClick={this._fillWithLuxuryCabins}>{translate('pcq')}</li> : ''}
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
</ul>
</div>;
}
}

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import ContainerDimensions from 'react-container-dimensions';
import * as d3 from 'd3';
import TranslatedComponent from './TranslatedComponent';
import autoBind from 'auto-bind';
const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
@@ -10,7 +11,6 @@ const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 };
* Line Chart
*/
export default class LineChart extends TranslatedComponent {
static defaultProps = {
code: '',
xMin: 0,
@@ -45,13 +45,7 @@ export default class LineChart extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
this._updateDimensions = this._updateDimensions.bind(this);
this._updateSeries = this._updateSeries.bind(this);
this._tooltip = this._tooltip.bind(this);
this._showTip = this._showTip.bind(this);
this._hideTip = this._hideTip.bind(this);
this._moveTip = this._moveTip.bind(this);
autoBind(this);
const series = props.series;

View File

@@ -7,7 +7,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
* Link wrapper component
*/
export default class Link extends React.Component {
static propTypes = {
children: PropTypes.any,
href: PropTypes.string.isRequired,
@@ -56,5 +55,4 @@ export default class Link extends React.Component {
render() {
return <a {...this.props} onClick={this.handler}>{this.props.children}</a>;
}
}

View File

@@ -9,7 +9,6 @@ import Persist from '../stores/Persist';
* Permalink modal
*/
export default class ModalBatchOrbis extends TranslatedComponent {
static propTypes = {
ships: PropTypes.any.isRequired
};

View File

@@ -21,7 +21,6 @@ function buildComparator(a, b) {
* Compare builds modal
*/
export default class ModalCompare extends TranslatedComponent {
static propTypes = {
onSelect: PropTypes.func.isRequired,
builds: PropTypes.array
@@ -105,8 +104,8 @@ export default class ModalCompare extends TranslatedComponent {
let selectedBuilds = usedBuilds.map((build, i) =>
<tr key={i} onClick={this._removeBuild.bind(this, i)}>
<td className='tl'>{build.name}</td><
td className='tl'>{build.buildName}</td>
<td className='tl'>{build.name}</td>
<td className='tl'>{build.buildName}</td>
</tr>
);

View File

@@ -6,7 +6,6 @@ import TranslatedComponent from './TranslatedComponent';
* Export Modal
*/
export default class ModalExport extends TranslatedComponent {
static propTypes = {
title: PropTypes.string,
generator: PropTypes.func,

View File

@@ -7,19 +7,10 @@ import TranslatedComponent from './TranslatedComponent';
* Help Modal
*/
export default class ModalHelp extends TranslatedComponent {
static propTypes = {
title: PropTypes.string
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
}
/**
* Render the modal
* @return {React.Component} Modal Content

View File

@@ -85,8 +85,6 @@ function detailedJsonToBuild(detailedBuild) {
* Import Modal
*/
export default class ModalImport extends TranslatedComponent {
static propTypes = {
builds: PropTypes.object, // Optional: Import object
};
@@ -130,7 +128,7 @@ export default class ModalImport extends TranslatedComponent {
if (data && data.Ship && data.Modules) {
const deflated = zlib.deflate(JSON.stringify(data), { to: 'string' });
let compressed = btoa(deflated);
this.setState({loadoutEvent: compressed});
this.setState({ loadoutEvent: compressed });
} else {
throw 'Loadout event must contain Ship and Modules';
}

View File

@@ -8,7 +8,6 @@ import Persist from '../stores/Persist';
* Permalink modal
*/
export default class ModalOrbis extends TranslatedComponent {
static propTypes = {
ship: PropTypes.any.isRequired
};
@@ -98,7 +97,7 @@ export default class ModalOrbis extends TranslatedComponent {
let ship = this.state.ship;
let cat = e.target.value;
ship.category = cat;
this.setState({ship});
this.setState({ ship });
}
/**

View File

@@ -7,7 +7,6 @@ import ShortenUrl from '../utils/ShortenUrl';
* Permalink modal
*/
export default class ModalPermalink extends TranslatedComponent {
static propTypes = {
url: PropTypes.string.isRequired
};

View File

@@ -8,7 +8,6 @@ import Persist from '../stores/Persist';
* Permalink modal
*/
export default class ModalShoppingList extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired
};

View File

@@ -3,131 +3,106 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames';
import NumberEditor from 'react-number-editor';
import { isChangeValueBeneficial } from '../utils/BlueprintFunctions';
import { Modifications } from 'coriolis-data/dist';
import { Module } from 'ed-forge';
/**
* Modification
*/
export default class Modification extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
m: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
onKeyDown: PropTypes.func.isRequired,
modItems: PropTypes.array.isRequired,
handleModChange: PropTypes.func.isRequired
highlight: PropTypes.bool,
m: PropTypes.instanceOf(Module).isRequired,
property: PropTypes.string.isRequired,
onSet: PropTypes.func.isRequired,
showProp: PropTypes.object,
onPropToggle: PropTypes.func.isRequired,
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
constructor(props) {
super(props);
this.state = {};
this.state.value = props.value;
const { m, property, showProp } = props;
const { beneficial, unit, value } = m.getFormatted(property, true);
this.state = { beneficial, unit, value, showProp };
}
/**
* Update modification given a value.
* @param {Number} value The value to set. This comes in as a string and must be stored in state as a string,
* because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing
* in a value by hand
*/
_updateValue(value) {
this.setState({ value });
let reCast = String(Number(value));
if (reCast.endsWith(value) || reCast.startsWith(value)) {
let { m, name, ship } = this.props;
value = Math.max(Math.min(value, 50000), -50000);
ship.setModification(m, name, value, true, true);
}
}
/**
* Triggered when a key is pressed down with focus on the number editor.
* @param {SyntheticEvent} event Key down event
*/
_keyDown(event) {
if (event.key == 'Enter') {
this._updateFinished();
}
this.props.onKeyDown(event);
}
/**
* Triggered when an update to slider value is finished i.e. when losing focus
*
* pnellesen (24/05/2018): added value check below - this should prevent experimental effects from being recalculated
* with each onBlur event, even when no change has actually been made to the field.
* Notify listeners that a new value has been entered and commited.
*/
_updateFinished() {
if (this.props.value != this.state.value) {
this.props.handleModChange(true);
this.props.onChange();
const { onSet, m, property } = this.props;
const { inputValue } = this.state;
const numValue = Number(inputValue);
if (!isNaN(numValue) && this.state.value !== numValue) {
onSet(property, numValue);
const { beneficial, unit, value } = m.getFormatted(property, true);
this.setState({ beneficial, unit, value });
}
}
_toggleProperty() {
const { onPropToggle, property } = this.props;
const showProp = !this.state.showProp;
// TODO: defer until menu closed
onPropToggle(property, showProp);
this.setState({ showProp });
}
/**
* Render the modification
* @return {React.Component} modification
*/
render() {
let { translate, formats, units } = this.context.language;
let { m, name } = this.props;
let modValue = m.getChange(name);
let isOverwrite = Modifications.modifications[name].method === 'overwrite';
const { formats } = this.context.language;
const { highlight, m, property } = this.props;
const { beneficial, unit, value, inputValue, showProp } = this.state;
if (name === 'damagedist') {
// We don't show damage distribution
// Some features only apply to specific modules; these features will be
// undefined on items that do not belong to the same class. Filter these
// features here
if (value === undefined) {
return null;
}
let inputClassNames = {
'cb': true,
'greyed-out': !this.props.highlight
};
const { value: modifierValue, unit: modifierUnit } = m.getModifierFormatted(property);
return (
<div onBlur={this._updateFinished.bind(this)} key={name}
className={cn('cb', 'modification-container')}
ref={ modItem => this.props.modItems[name] = modItem }>
<span className={'cb'}>{translate(name, m.grp)}</span>
<span className={'header-adjuster'}></span>
<table style={{ width: '100%' }}>
<tbody>
<tr>
<td className={'input-container'}>
<td>
<span>
{this.props.editable ?
<NumberEditor className={cn(inputClassNames)} value={this.state.value}
decimals={2} style={{ textAlign: 'right' }} step={0.01}
stepModifier={1} onKeyDown={this._keyDown.bind(this)}
onValueChange={this._updateValue.bind(this)} /> :
<input type="text" value={formats.f2(this.state.value)}
disabled className={cn('number-editor', 'greyed-out')}
style={{ textAlign: 'right', cursor: 'inherit' }}/>
<input type="checkbox" checked={showProp} onClick={() => this._toggleProperty()}/>
</span>
</td>
<td className="input-container">
<span>
<NumberEditor value={inputValue || value} stepModifier={1}
decimals={2} step={0.01} style={{ textAlign: 'right', width: '100%' }}
className={cn('cb', { 'greyed-out': !highlight })}
onKeyDown={(event) => {
if (event.key == 'Enter') {
this._updateFinished();
event.stopPropagation();
}
<span className={'unit-container'}>
{units[m.getStoredUnitFor(name)]}
</span>
}}
onValueChange={(inputValue) => {
if (inputValue.length <= 15) {
this.setState({ inputValue });
}
}} />
</span>
</td>
<td style={{ textAlign: 'center' }} className={
modValue ?
isChangeValueBeneficial(name, modValue) ? 'secondary' : 'warning' :
''
}>
{formats.f2(modValue / 100) || 0}{isOverwrite ? '' : '%'}
<td style={{ textAlign: 'left' }}>
<span className="unit-container">{unit}</span>
</td>
<td style={{ textAlign: 'center' }}
className={cn({
secondary: beneficial,
warning: beneficial === false,
})}
>{formats.f2(modifierValue)}{modifierUnit || ''}</td>
</tr>
</tbody>
</table>
</div>
);
}
}

View File

@@ -1,34 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import * as _ from 'lodash';
import { chain, flatMap, keys } from 'lodash';
import TranslatedComponent from './TranslatedComponent';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import cn from 'classnames';
import { Modifications } from 'coriolis-data/dist';
import Modification from './Modification';
import {
getBlueprint,
blueprintTooltip,
setPercent,
getPercent,
setRandom,
specialToolTip
} from '../utils/BlueprintFunctions';
const MODIFICATIONS_COMPARATOR = (mod1, mod2) => {
return mod1.props.name.localeCompare(mod2.props.name);
};
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/data/blueprints';
import { getModuleInfo } from 'ed-forge/lib/data/items';
import { SHOW } from '../shipyard/StatsMapping';
/**
* Modifications menu
*/
export default class ModificationsMenu extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
className: PropTypes.string,
m: PropTypes.object.isRequired,
marker: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
modButton:PropTypes.object
propsToShow: PropTypes.object.isRequired,
onPropToggle: PropTypes.func.isRequired,
};
/**
@@ -41,122 +34,58 @@ export default class ModificationsMenu extends TranslatedComponent {
this._toggleBlueprintsMenu = this._toggleBlueprintsMenu.bind(this);
this._toggleSpecialsMenu = this._toggleSpecialsMenu.bind(this);
this._rollFifty = this._rollFifty.bind(this);
this._rollRandom = this._rollRandom.bind(this);
this._rollBest = this._rollBest.bind(this);
this._rollWorst = this._rollWorst.bind(this);
this._reset = this._reset.bind(this);
this._keyDown = this._keyDown.bind(this);
this.modItems = [];// Array to hold various element refs (<li>, <div>, <ul>, etc.)
this.firstModId = null;
this.firstBPLabel = null;// First item in mod menu
this.lastModId = null;
this.selectedModId = null;
this.selectedSpecialId = null;
this.lastNeId = null;// Last number editor id. Used to set focus to last number editor when shift-tab pressed on first element in mod menu.
this.modValDidChange = false; // used to determine if component update was caused by change in modification value.
this._handleModChange = this._handleModChange.bind(this);
this.selectedModRef = null;
this.selectedSpecialRef = null;
const { m } = props;
this.state = {
blueprintMenuOpened: !(props.m.blueprint && props.m.blueprint.name),
blueprintProgress: m.getBlueprintProgress(),
blueprintMenuOpened: !m.getBlueprint(),
specialMenuOpened: false
};
}
/**
* Render the blueprints
* @param {Object} props React component properties
* @param {Object} context React component context
* @return {Object} list: Array of React Components
*/
_renderBlueprints(props, context) {
const { m } = props;
const { language, tooltip, termtip } = context;
const translate = language.translate;
const blueprints = [];
for (const blueprintName in Modifications.modules[m.grp].blueprints) {
const blueprint = getBlueprint(blueprintName, m);
let blueprintGrades = [];
for (let grade in Modifications.modules[m.grp].blueprints[blueprintName].grades) {
_renderBlueprints() {
const { m } = this.props;
const { language, tooltip, termtip } = this.context;
const { translate } = language;
const blueprints = m.getApplicableBlueprints().map(blueprint => {
const info = getBlueprintInfo(blueprint);
let blueprintGrades = keys(info.features).map(grade => {
// Grade is a string in the JSON so make it a number
grade = Number(grade);
const classes = cn('c', {
active: m.blueprint && blueprint.id === m.blueprint.id && grade === m.blueprint.grade
const active = m.getBlueprint() === blueprint && m.getBlueprintGrade() === grade;
const key = blueprint + ':' + grade;
return <li key={key} data-id={key} className={cn('c', { active })}
style={{ width: '2em' }}
onMouseOver={termtip.bind(null, blueprintTooltip(language, m, blueprint, grade))}
onMouseOut={tooltip.bind(null, null)}
onClick={() => {
m.setBlueprint(blueprint, grade, 1);
this.setState({
blueprintMenuOpened: false,
specialMenuOpened: true,
});
}}
ref={active ? (ref) => { this.selectedModRef = ref; } : undefined}
>{grade}</li>;
});
const close = this._blueprintSelected.bind(this, blueprintName, grade);
const key = blueprintName + ':' + grade;
const tooltipContent = blueprintTooltip(translate, blueprint.grades[grade], Modifications.modules[m.grp].blueprints[blueprintName].grades[grade].engineers, m.grp);
if (classes.indexOf('active') >= 0) this.selectedModId = key;
blueprintGrades.unshift(<li key={key} tabIndex="0" data-id={key} className={classes} style={{ width: '2em' }} onMouseOver={termtip.bind(null, tooltipContent)} onMouseOut={tooltip.bind(null, null)} onClick={close} onKeyDown={this._keyDown} ref={modItem => this.modItems[key] = modItem}>{grade}</li>);
}
if (blueprintGrades) {
const thisLen = blueprintGrades.length;
if (this.firstModId == null) this.firstModId = blueprintGrades[0].key;
this.lastModId = blueprintGrades[thisLen - 1].key;
blueprints.push(<div key={blueprint.name} className={'select-group cap'}>{translate(blueprint.name)}</div>);
blueprints.push(<ul key={blueprintName}>{blueprintGrades}</ul>);
}
}
return blueprints;
}
/**
* Key down - select module on Enter key, move to next/previous module on Tab/Shift-Tab, close on Esc
* @param {SyntheticEvent} event Event
*
*/
_keyDown(event) {
let className = null;
let elemId = null;
if (event.currentTarget.attributes['class']) className = event.currentTarget.attributes['class'].value;
if (event.currentTarget.attributes['data-id']) elemId = event.currentTarget.attributes['data-id'].value;
return [
<div key={'div' + blueprint} className={'select-group cap'}>
{translate(blueprint)}
</div>,
<ul key={'ul' + blueprint}>{blueprintGrades}</ul>
];
});
if (event.key == 'Enter' && className.indexOf('disabled') < 0 && className.indexOf('active') < 0) {
event.stopPropagation();
if (elemId != null) {
this.modItems[elemId].click();
} else {
event.currentTarget.click();
return flatMap(blueprints);
}
return;
}
if (event.key == 'Tab') {
// Shift-Tab
if(event.shiftKey) {
if (elemId == this.firstModId && elemId != null) {
// Initial modification menu
event.preventDefault();
this.modItems[this.lastModId].focus();
return;
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null && this.lastNeId != null && this.modItems[this.lastNeId] != null) {
// shift-tab on first element in modifications menu. set focus to last number editor field if open
event.preventDefault();
this.modItems[this.lastNeId].lastChild.focus();
return;
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.previousElementSibling == null) {
// shift-tab on button-inline-menu with no number editor
event.preventDefault();
event.currentTarget.parentElement.lastElementChild.focus();
}
} else {
if (elemId == this.lastModId && elemId != null) {
// Initial modification menu
event.preventDefault();
this.modItems[this.firstModId].focus();
return;
} else if (event.currentTarget.className.indexOf('button-inline-menu') >= 0 && event.currentTarget.nextSibling == null && event.currentTarget.nodeName != 'TD') {
// Experimental menu
event.preventDefault();
event.currentTarget.parentElement.firstElementChild.focus();
return;
} else if (event.currentTarget.className == 'cb' && event.currentTarget.parentElement.nextSibling == null) {
event.preventDefault();
this.modItems[this.firstBPLabel].focus();
}
}
}
}
/**
* Render the specials
@@ -164,204 +93,118 @@ export default class ModificationsMenu extends TranslatedComponent {
* @param {Object} context React component context
* @return {Object} list: Array of React Components
*/
_renderSpecials(props, context) {
const { m } = props;
const { language, tooltip, termtip } = context;
_renderSpecials() {
const { m } = this.props;
const { language, tooltip, termtip } = this.context;
const translate = language.translate;
const specials = [];
const specialsId = m.missile && Modifications.modules[m.grp]['specials_' + m.missile] ? 'specials_' + m.missile : 'specials';
if (Modifications.modules[m.grp][specialsId] && Modifications.modules[m.grp][specialsId].length > 0) {
const close = this._specialSelected.bind(this, null);
specials.push(<div tabIndex="0" style={{ cursor: 'pointer', fontWeight: 'bold' }} className={ 'button-inline-menu warning' } key={ 'none' } data-id={ 'none' } onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems['none'] = modItem}>{translate('PHRASE_NO_SPECIAL')}</div>);
for (const specialName of Modifications.modules[m.grp][specialsId]) {
if (Modifications.specials[specialName].name.search('Legacy') >= 0) {
continue;
const applied = m.getExperimental();
const experimentals = [];
for (const experimental of m.getApplicableExperimentals()) {
const active = experimental === applied;
let specialTt = specialToolTip(language, m, experimental);
experimentals.push(
<div key={experimental} data-id={experimental}
style={{ cursor: 'pointer' }}
className={cn('button-inline-menu', { active })}
onClick={this._specialSelected(experimental)}
ref={active ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
onMouseOver={termtip.bind(null, specialTt)}
onMouseOut={tooltip.bind(null, null)}
>{translate(experimental)}</div>
);
}
const classes = cn('button-inline-menu', {
active: m.blueprint && m.blueprint.special && m.blueprint.special.edname == specialName
});
if (classes.indexOf('active') >= 0) this.selectedSpecialId = specialName;
const close = this._specialSelected.bind(this, specialName);
if (m.blueprint && m.blueprint.name) {
let tmp = {};
if (m.blueprint.special) {
tmp = m.blueprint.special;
} else {
tmp = undefined;
if (experimentals.length) {
experimentals.unshift(
<div style={{ cursor: 'pointer', fontWeight: 'bold' }}
className="button-inline-menu warning" key="none" data-id="none"
// Setting the special effect to undefined clears it
onClick={this._specialSelected(undefined)}
ref={!applied ? (ref) => { this.selectedSpecialRef = ref; } : undefined}
>{translate('PHRASE_NO_SPECIAL')}</div>
);
}
m.blueprint.special = Modifications.specials[specialName];
let specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, specialName);
m.blueprint.special = tmp;
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName } onMouseOver={termtip.bind(null, specialTt)} onMouseOut={tooltip.bind(null, null)} onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
} else {
specials.push(<div tabIndex="0" style={{ cursor: 'pointer' }} className={classes} key={ specialName } data-id={ specialName }onClick={ close } onKeyDown={this._keyDown} ref={modItem => this.modItems[specialName] = modItem}>{translate(Modifications.specials[specialName].name)}</div>);
return experimentals;
}
/**
* Create a modification component
*/
_mkModification(property, highlight) {
const { translate } = this.context.language;
const { m, propsToShow, onPropToggle } = this.props;
let onSet = m.set.bind(m);
// Show resistance instead of effectiveness
const mapped = SHOW[property];
if (mapped) {
property = mapped.as;
onSet = mapped.setter.bind(undefined, m);
}
}
return specials;
return [
<tr key={`th-${property}`}>
<th colSpan="4">
<span className="cb">{translate(property)}</span>
</th>
</tr>,
<Modification key={property} m={m} property={property}
onSet={onSet} highlight={highlight} showProp={propsToShow[property]}
onPropToggle={onPropToggle} />
];
}
/**
* Render the modifications
* @param {Object} props React Component properties
* @return {Object} list: Array of React Components
* @return {Array} Array of React Components
*/
_renderModifications(props) {
const { m, onChange, ship } = props;
const modifiableModifications = [];
const modifications = [];
for (const modName of Modifications.modules[m.grp].modifications) {
if (!Modifications.modifications[modName].hidden) {
const key = modName + (m.getModValue(modName) / 100 || 0);
const editable = modName !== 'fallofffromrange';
const highlight = m.blueprint.grades[m.blueprint.grade].features[modName];
this.lastNeId = modName;
(editable && highlight ? modifiableModifications : modifications).push(
<Modification key={ key } ship={ ship } m={ m } highlight={highlight}
value={m.getPretty(modName) || 0} modItems={this.modItems}
onChange={onChange} onKeyDown={this._keyDown} name={modName}
editable={editable} handleModChange = {this._handleModChange} />
);
}
}
_renderModifications() {
const { m } = this.props;
modifiableModifications.sort(MODIFICATIONS_COMPARATOR);
modifications.sort(MODIFICATIONS_COMPARATOR);
return modifiableModifications.concat(modifications);
const blueprintFeatures = getBlueprintInfo(m.getBlueprint()).features[
m.getBlueprintGrade()
];
const blueprintModifications = chain(keys(blueprintFeatures))
.map((feature) => this._mkModification(feature, true))
.filter(([_, mod]) => Boolean(mod))
.flatMap()
.value();
const moduleModifications = chain(keys(getModuleInfo(m.getItem()).props))
.filter((prop) => !blueprintFeatures[prop])
.map((prop) => this._mkModification(prop, false))
.flatMap()
.value();
return blueprintModifications.concat(moduleModifications);
}
/**
* Toggle the blueprints menu
*/
_toggleBlueprintsMenu() {
const blueprintMenuOpened = !this.state.blueprintMenuOpened;
this.setState({ blueprintMenuOpened });
}
/**
* Activated when a blueprint is selected
* @param {int} fdname The Frontier name of the blueprint
* @param {int} grade The grade of the selected blueprint
*/
_blueprintSelected(fdname, grade) {
this.context.tooltip(null);
const { m, ship } = this.props;
const blueprint = getBlueprint(fdname, m);
blueprint.grade = grade;
ship.setModuleBlueprint(m, blueprint);
setPercent(ship, m, 100);
this.setState({ blueprintMenuOpened: false, specialMenuOpened: true });
this.props.onChange();
this.setState({ blueprintMenuOpened: !this.state.blueprintMenuOpened });
}
/**
* Toggle the specials menu
*/
_toggleSpecialsMenu() {
const specialMenuOpened = !this.state.specialMenuOpened;
this.setState({ specialMenuOpened });
this.setState({ specialMenuOpened: !this.state.specialMenuOpened });
}
/**
* Activated when a special is selected
* @param {int} special The name of the selected special
* Creates a callback for when a special effect is being selected
* @param {string} special The name of the selected special
* @returns {function} Callback
*/
_specialSelected(special) {
this.context.tooltip(null);
const { m, ship } = this.props;
if (special === null) {
ship.clearModuleSpecial(m);
} else {
ship.setModuleSpecial(m, Modifications.specials[special]);
}
return () => {
const { m } = this.props;
m.setExperimental(special);
this.setState({ specialMenuOpened: false });
this.props.onChange();
}
/**
* Provide a '50%' roll within the information we have
*/
_rollFifty() {
const { m, ship } = this.props;
setPercent(ship, m, 50);
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
this._handleModChange(true);
this.props.onChange();
}
/**
* Provide a random roll within the information we have
*/
_rollRandom() {
const { m, ship } = this.props;
setRandom(ship, m);
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
this._handleModChange(true);
this.props.onChange();
}
/**
* Provide a 'best' roll within the information we have
*/
_rollBest() {
const { m, ship } = this.props;
setPercent(ship, m, 100);
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
this._handleModChange(true);
this.props.onChange();
}
/**
* Provide a 'worst' roll within the information we have
*/
_rollWorst() {
const { m, ship } = this.props;
setPercent(ship, m, 0);
// this will change the values in the modifications. Set modDidChange to true to prevent focus change when component updates
this._handleModChange(true);
this.props.onChange();
}
/**
* Reset modification information
*/
_reset() {
const { m, ship } = this.props;
ship.clearModifications(m);
ship.clearModuleBlueprint(m);
this.selectedModId = null;
this.selectedSpecialId = null;
this.props.onChange();
}
/**
* set mod did change boolean
* @param {boolean} b Boolean to determine if a change has been made to a module
*/
_handleModChange(b) {
this.modValDidChange = b;
}
/**
* Set focus on first element in modifications menu
* after it first mounts
*/
componentDidMount() {
let firstEleCn = this.modItems['modMainDiv'].children.length > 0 ? this.modItems['modMainDiv'].children[0].className : null;
if (firstEleCn.indexOf('select-group cap') >= 0) {
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
} else {
this.modItems['modMainDiv'].firstElementChild.focus();
}
};
}
/**
@@ -370,33 +213,13 @@ export default class ModificationsMenu extends TranslatedComponent {
* in a modification
*/
componentDidUpdate() {
if (!this.modValDidChange) {
if (this.modItems['modMainDiv'].children.length > 0) {
if (this.modItems[this.selectedModId]) {
this.modItems[this.selectedModId].focus();
if (this.selectedModRef) {
this.selectedModRef.focus();
return;
} else if (this.modItems[this.selectedSpecialId]) {
this.modItems[this.selectedSpecialId].focus();
} else if (this.selectedSpecialRef) {
this.selectedSpecialRef.focus();
return;
}
let firstEleCn = this.modItems['modMainDiv'].children[0].className;
if (firstEleCn.indexOf('button-inline-menu') >= 0) {
this.modItems['modMainDiv'].firstElementChild.focus();
} else if (firstEleCn.indexOf('select-group cap') >= 0) {
this.modItems['modMainDiv'].children[1].firstElementChild.focus();
}
}
} else {
this._handleModChange(false);// Need to reset if component update due to value change
}
}
/**
* set focus to the modification menu icon after mod menu is unmounted.
*/
componentWillUnmount() {
if (this.props.modButton) {
this.props.modButton.focus();
}
}
/**
@@ -407,90 +230,155 @@ export default class ModificationsMenu extends TranslatedComponent {
const { language, tooltip, termtip } = this.context;
const translate = language.translate;
const { m } = this.props;
const { blueprintMenuOpened, specialMenuOpened } = this.state;
const {
blueprintProgress, blueprintMenuOpened, specialMenuOpened,
} = this.state;
const _toggleBlueprintsMenu = this._toggleBlueprintsMenu;
const _toggleSpecialsMenu = this._toggleSpecialsMenu;
const _rollFull = this._rollBest;
const _rollWorst = this._rollWorst;
const _rollFifty = this._rollFifty;
const _rollRandom = this._rollRandom;
const _reset = this._reset;
const appliedBlueprint = m.getBlueprint();
const appliedGrade = m.getBlueprintGrade();
const appliedExperimental = m.getExperimental();
let blueprintLabel;
let haveBlueprint = false;
let blueprintTt;
let blueprintCv;
// TODO: Fix this to actually find the correct blueprint.
if (!m.blueprint || !m.blueprint.name || !m.blueprint.fdname || !Modifications.modules[m.grp].blueprints || !Modifications.modules[m.grp].blueprints[m.blueprint.fdname]) {
this.props.ship.clearModuleBlueprint(m);
this.props.ship.clearModuleSpecial(m);
}
if (m.blueprint && m.blueprint.name && Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade]) {
blueprintLabel = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
haveBlueprint = true;
blueprintTt = blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], Modifications.modules[m.grp].blueprints[m.blueprint.fdname].grades[m.blueprint.grade].engineers, m.grp);
blueprintCv = getPercent(m);
}
let renderComponents = [];
switch (true) {
case !appliedBlueprint || blueprintMenuOpened:
renderComponents = this._renderBlueprints();
break;
case specialMenuOpened:
renderComponents = this._renderSpecials();
break;
default:
// Since the first case didn't apply, there is a blueprint applied so
// we render the modifications
let blueprintTt = blueprintTooltip(language, m, appliedBlueprint, appliedGrade);
let specialLabel;
let specialTt;
if (m.blueprint && m.blueprint.special) {
specialLabel = m.blueprint.special.name;
specialTt = specialToolTip(translate, m.blueprint.grades[m.blueprint.grade], m.grp, m, m.blueprint.special.edname);
} else {
specialLabel = translate('PHRASE_SELECT_SPECIAL');
}
const specials = this._renderSpecials(this.props, this.context);
/**
* pnellesen - 05/28/2018 - added additional checks for specials.length below to ensure menus
* display correctly in cases where there are no specials (ex: AFMUs.)
*/
const showBlueprintsMenu = blueprintMenuOpened;
const showSpecial = haveBlueprint && specials.length && !blueprintMenuOpened;
const showSpecialsMenu = specialMenuOpened && specials.length;
const showRolls = haveBlueprint && !blueprintMenuOpened && (!specialMenuOpened || !specials.length);
const showReset = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
const showMods = !blueprintMenuOpened && (!specialMenuOpened || !specials.length) && haveBlueprint;
if (haveBlueprint) {
this.firstBPLabel = blueprintLabel;
} else {
this.firstBPLabel = 'selectBP';
}
return (
<div
className={cn('select', this.props.className)}
onClick={(e) => e.stopPropagation() }
onContextMenu={stopCtxPropagation}
ref={modItem => this.modItems['modMainDiv'] = modItem}
renderComponents.push(
<div style={{ cursor: 'pointer' }} key="blueprintsMenu"
className="section-menu button-inline-menu"
onMouseOver={termtip.bind(null, blueprintTt)}
onMouseOut={tooltip.bind(null, null)}
onClick={this._toggleBlueprintsMenu}
>
{ showBlueprintsMenu | showSpecialsMenu ? '' : haveBlueprint ?
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={termtip.bind(null, blueprintTt)} onMouseOut={tooltip.bind(null, null)} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{blueprintLabel}</div> :
<div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: blueprintMenuOpened })} style={{ cursor: 'pointer' }} onClick={_toggleBlueprintsMenu} onKeyDown={ this._keyDown } ref={modItems => this.modItems[this.firstBPLabel] = modItems}>{translate('PHRASE_SELECT_BLUEPRINT')}</div> }
{ showBlueprintsMenu ? this._renderBlueprints(this.props, this.context) : null }
{ showSpecial & !showSpecialsMenu ? <div tabIndex="0" className={ cn('section-menu button-inline-menu', { selected: specialMenuOpened })} style={{ cursor: 'pointer' }} onMouseOver={specialTt ? termtip.bind(null, specialTt) : null} onMouseOut={specialTt ? tooltip.bind(null, null) : null} onClick={_toggleSpecialsMenu} onKeyDown={ this._keyDown }>{specialLabel}</div> : null }
{ showSpecialsMenu ? specials : null }
{ showReset ? <div tabIndex="0" className={'section-menu button-inline-menu warning'} style={{ cursor: 'pointer' }} onClick={_reset} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')} onMouseOut={tooltip.bind(null, null)}> { translate('reset') } </div> : null }
{ showRolls ?
{translate(appliedBlueprint)} {translate('grade')} {appliedGrade}
</div>
);
if (m.getApplicableExperimentals().length) {
let specialLabel = translate('PHRASE_SELECT_SPECIAL');
let specialTt;
if (appliedExperimental) {
specialLabel = appliedExperimental;
specialTt = specialToolTip(language, m, appliedExperimental);
}
renderComponents.push(
<div className="section-menu button-inline-menu"
style={{ cursor: 'pointer' }}
onMouseOver={specialTt ? termtip.bind(null, specialTt) : null}
onMouseOut={specialTt ? tooltip.bind(null, null) : null}
onClick={this._toggleSpecialsMenu}
>{specialLabel}</div>
);
}
renderComponents.push(
<div
className="section-menu button-inline-menu warning"
style={{ cursor: 'pointer' }}
onClick={() => {
m.resetEngineering();
this.selectedModRef = null;
this.selectedSpecialRef = null;
tooltip(null);
this.setState({
blueprintMenuOpened: true,
blueprintProgress: undefined,
});
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RESET')}
onMouseOut={tooltip.bind(null, null)}
>{translate('reset')}</div>,
<table style={{ width: '100%', backgroundColor: 'transparent' }}>
<tbody>
{ showRolls ?
<tr>
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: false }) }> { translate('mroll') }: </td>
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 0 }) } style={{ cursor: 'pointer' }} onClick={_rollWorst} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')} onMouseOut={tooltip.bind(null, null)}> { translate('0%') } </td>
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 50 })} style={{ cursor: 'pointer' }} onClick={_rollFifty} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')} onMouseOut={tooltip.bind(null, null)}> { translate('50%') } </td>
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === 100 })} style={{ cursor: 'pointer' }} onClick={_rollFull} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')} onMouseOut={tooltip.bind(null, null)}> { translate('100%') } </td>
<td tabIndex="0" className={ cn('section-menu button-inline-menu', { active: blueprintCv === null || blueprintCv % 50 != 0 })} style={{ cursor: 'pointer' }} onClick={_rollRandom} onKeyDown={ this._keyDown } onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')} onMouseOut={tooltip.bind(null, null)}> { translate('random') } </td>
</tr> : null }
<td
className={cn(
'section-menu button-inline-menu',
{ active: false },
)}
>{translate('mroll')}:</td>
<td
className={cn(
'section-menu button-inline-menu',
{ active: blueprintProgress === 0 },
)} style={{ cursor: 'pointer' }}
onClick={() => {
m.setBlueprintProgress(0);
this.setState({ blueprintProgress: 0 });
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_WORST')}
onMouseOut={tooltip.bind(null, null)}
>{translate('0%')}</td>
<td
className={cn(
'section-menu button-inline-menu',
{ active: blueprintProgress === 0.5 },
)} style={{ cursor: 'pointer' }}
onClick={() => {
m.setBlueprintProgress(0.5);
this.setState({ blueprintProgress: 0.5 });
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_FIFTY')}
onMouseOut={tooltip.bind(null, null)}
>{translate('50%')}</td>
<td
className={cn(
'section-menu button-inline-menu',
{ active: blueprintProgress === 1 },
)}
style={{ cursor: 'pointer' }}
onClick={() => {
m.setBlueprintProgress(1);
this.setState({ blueprintProgress: 1 });
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_BEST')}
onMouseOut={tooltip.bind(null, null)}
>{translate('100%')}</td>
<td
className={cn(
'section-menu button-inline-menu',
{ active: blueprintProgress % 0.5 !== 0 },
)}
style={{ cursor: 'pointer' }}
onClick={() => {
const blueprintProgress = Math.random();
m.setBlueprintProgress(blueprintProgress);
this.setState({ blueprintProgress });
}}
onMouseOver={termtip.bind(null, 'PHRASE_BLUEPRINT_RANDOM')}
onMouseOut={tooltip.bind(null, null)}
>{translate('random')}</td>
</tr>
</tbody>
</table> : null }
{ showMods ? <hr /> : null }
{ showMods ?
<span onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)} >
{ this._renderModifications(this.props) }
</span> : null }
</table>,
<hr />,
<span
onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')}
onMouseOut={tooltip.bind(null, null)}
>
<table style={{ width: '100%' }}>
<tbody>
{this._renderModifications()}
</tbody>
</table>
</span>
);
}
return (
<div className={cn('select', this.props.className)}
onClick={(e) => e.stopPropagation()}
onContextMenu={stopCtxPropagation}
>
{renderComponents}
</div>
);
}

View File

@@ -1,36 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import { ShipProps } from 'ed-forge';
const { SPEED, BOOST_SPEED, ROLL, BOOST_ROLL, YAW, BOOST_YAW, PITCH, BOOST_PITCH } = ShipProps;
/**
* Movement
*/
export default class Movement extends TranslatedComponent {
static propTypes = {
marker: PropTypes.string.isRequired,
code: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
boost: PropTypes.bool.isRequired,
eng: PropTypes.number.isRequired,
fuel: PropTypes.number.isRequired,
cargo: PropTypes.number.isRequired
pips: PropTypes.object.isRequired,
};
/**
* Constructor
* @param {Object} props React Component properties
*/
constructor(props) {
super(props);
}
/**
* Render movement
* @return {React.Component} contents
*/
render() {
const { ship, boost, eng, cargo, fuel } = this.props;
const { ship, boost } = this.props;
const { language } = this.context;
const { formats, translate, units } = language;
const { formats } = language;
return (
<span id='movement'>
@@ -57,14 +49,10 @@ export default class Movement extends TranslatedComponent {
<path d="M359.5 422.4l-1.2 19.3-1.6.4-10.7-16 .2-.2 13-3.4.3.4zm-9 5l5.2 7.8.6-9.3-5.7 1.2zm-10.5 24l-13.2 8.6-2.6-9.7 15.8 1z"/>
<path d="M342 450l.4 1.5-16.2 10.7-.4-.2-3.5-13 .3-.3L342 450zm-14.3 7.6l7.7-5-9.2-.6 1.5 5.6z"/>
{/* Speed */}
<text x="470" y="30" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcSpeed(eng, fuel, cargo, boost)) + 'm/s' : '-'}</text>
{/* Pitch */}
<text x="355" y="410" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcPitch(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
{/* Roll */}
<text x="450" y="110" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcRoll(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
{/* Yaw */}
<text x="160" y="430" strokeWidth='0'>{ship.canThrust(cargo, fuel) ? formats.int(ship.calcYaw(eng, fuel, cargo, boost)) + '°/s' : '-'}</text>
<text x="470" y="30" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_SPEED : SPEED)) + 'm/s'}</text>
<text x="355" y="410" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_PITCH : PITCH)) + '°/s'}</text>
<text x="450" y="110" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_ROLL : ROLL)) + '°/s'}</text>
<text x="160" y="430" strokeWidth='0'>{formats.int(ship.get(boost ? BOOST_YAW : YAW)) + '°/s'}</text>
</svg>
</span>);
}

View File

@@ -3,101 +3,36 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import * as Calc from '../shipyard/Calculations';
import PieChart from './PieChart';
import { nameComparator } from '../utils/SlotFunctions';
import { MountFixed, MountGimballed, MountTurret } from './SvgIcons';
import { Ship } from 'ed-forge';
import autoBind from 'auto-bind';
import { DAMAGE_METRICS } from 'ed-forge/lib/ship-stats';
import { clone, mapValues, mergeWith, reverse, sortBy, sum, toPairs, values } from 'lodash';
/**
* Generates an internationalization friendly weapon comparator that will
* sort by specified property (if provided) then by name/group, class, rating
* @param {function} translate Translation function
* @param {function} propComparator Optional property comparator
* @param {boolean} desc Use descending order
* @return {function} Comparator function for names
* Turns an object into a tooltip.
* @param {function} translate Translate function
* @param {object} o Map to make the tooltip from
* @returns {React.Component} Tooltip
*/
export function weaponComparator(translate, propComparator, desc) {
return (a, b) => {
if (!desc) { // Flip A and B if ascending order
let t = a;
a = b;
b = t;
}
// If a property comparator is provided use it first
let diff = propComparator ? propComparator(a, b) : nameComparator(translate, a, b);
if (diff) {
return diff;
}
// Property matches so sort by name / group, then class, rating
if (a.name === b.name && a.grp === b.grp) {
if(a.class == b.class) {
return a.rating > b.rating ? 1 : -1;
}
return a.class - b.class;
}
return nameComparator(translate, a, b);
};
}
/**
* Creates a tooltip that shows damage by type.
* @param {function} translate Translation function
* @param {Object} formats Object that holds format functions
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
* @returns {Array} Tooltip
*/
function getSDpsTooltip(translate, formats, sdpsObject) {
return Object.keys(sdpsObject).filter(key => sdpsObject[key])
.map(key => {
return (
<div key={key}>
{translate(key) + ' ' + formats.f1(sdpsObject[key])}
</div>
);
});
function objToTooltip(translate, o) {
return toPairs(o)
.filter(([k, v]) => Boolean(v))
.map(([k, v]) => <div key={k}>{`${translate(k)}: ${v}`}</div>);
}
/**
* Returns a data object used by {@link PieChart} that shows damage by type.
* @param {function} translate Translation function
* @param {Calc.SDps} sdpsObject Object that holds sdps split up by type
* @param {Calc.SDps} o Object that holds sdps split up by type
* @returns {Object} Data object
*/
function getSDpsData(translate, sdpsObject) {
return Object.keys(sdpsObject).map(key => {
return {
value: Math.round(sdpsObject[key]),
label: translate(key)
};
function objToPie(translate, o) {
return toPairs(o).map(([k, value]) => {
return { label: translate(k), value };
});
}
/**
* Adds all damage of `add` onto `addOn`.
* @param {Calc.SDps} addOn Object that holds sdps split up by type (will be mutated)
* @param {Calc.SDps} add Object that holds sdps split up by type
*/
function addSDps(addOn, add) {
Object.keys(addOn).map(k => addOn[k] += (add[k] ? add[k] : 0));
}
/**
* Calculates the overall sdps of an sdps object.
* @param {Calc.SDps} sdpsObject Object that holds sdps spluit up by type
*/
function sumSDps(sdpsObject) {
if (sdpsObject.total) {
return sdpsObject.total;
}
return Object.keys(sdpsObject).reduce(
(acc, k) => acc + (sdpsObject[k] ? sdpsObject[k] : 0),
0
);
}
/**
* Offence information
* Offence information consists of four panels:
@@ -108,12 +43,10 @@ function sumSDps(sdpsObject) {
*/
export default class Offence extends TranslatedComponent {
static propTypes = {
marker: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
opponent: PropTypes.object.isRequired,
engagementrange: PropTypes.number.isRequired,
wep: PropTypes.number.isRequired,
opponentSys: PropTypes.number.isRequired
code: PropTypes.string.isRequired,
ship: PropTypes.instanceOf(Ship).isRequired,
opponent: PropTypes.instanceOf(Ship).isRequired,
engagementRange: PropTypes.number.isRequired,
};
/**
@@ -122,146 +55,196 @@ export default class Offence extends TranslatedComponent {
*/
constructor(props) {
super(props);
autoBind(this);
this._sort = this._sort.bind(this);
const damage = Calc.offenceMetrics(props.ship, props.opponent, props.wep, props.opponentSys, props.engagementrange);
this.state = {
predicate: 'n',
predicate: 'classRating',
desc: true,
damage
};
}
/**
* Update the state if our properties change
* @param {Object} nextProps Incoming/Next properties
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps) {
if (this.props.marker != nextProps.marker || this.props.eng != nextProps.eng) {
const damage = Calc.offenceMetrics(nextProps.ship, nextProps.opponent, nextProps.wep, nextProps.opponentSys, nextProps.engagementrange);
this.setState({ damage });
}
return true;
}
/**
* Set the sort order and sort
* @param {string} predicate Sort predicate
*/
_sortOrder(predicate) {
let desc = this.state.desc;
if (predicate == this.state.predicate) {
desc = !desc;
} else {
desc = true;
}
this._sort(predicate, desc);
let desc = predicate == this.state.predicate ? !this.state.desc : true;
this.setState({ predicate, desc });
}
/**
* Sorts the weapon list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort order descending
*/
_sort(predicate, desc) {
let comp = weaponComparator.bind(null, this.context.language.translate);
switch (predicate) {
case 'n': comp = comp(null, desc); break;
case 'esdpss': comp = comp((a, b) => a.sdps.shields.total - b.sdps.shields.total, desc); break;
case 'es': comp = comp((a, b) => a.effectiveness.shields.total - b.effectiveness.shields.total, desc); break;
case 'esdpsh': comp = comp((a, b) => a.sdps.armour.total - b.sdps.armour.total, desc); break;
case 'eh': comp = comp((a, b) => a.effectiveness.armour.total - b.effectiveness.armour.total, desc); break;
}
this.state.damage.sort(comp);
}
/**
* Render offence
* @return {React.Component} contents
*/
render() {
const { ship, opponent, wep, engagementrange } = this.props;
const { ship } = this.props;
const { language, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { damage } = this.state;
const sortOrder = this._sortOrder;
const pd = ship.standard[4].m;
const damage = ship.getMetrics(DAMAGE_METRICS);
const portions = {
Absolute: damage.types.abs,
Explosive: damage.types.expl,
Kinetic: damage.types.kin,
Thermic: damage.types.therm,
};
const opponentShields = Calc.shieldMetrics(opponent, 4);
const opponentArmour = Calc.armourMetrics(opponent);
const oppShield = ship.getOpponent().getShield();
const shieldMults = {
Absolute: 1,
Explosive: oppShield.explosive.damageMultiplier,
Kinetic: oppShield.kinetic.damageMultiplier,
Thermic: oppShield.thermal.damageMultiplier,
};
const timeToDrain = Calc.timeToDrainWep(ship, wep);
const oppArmour = ship.getOpponent().getArmour();
const armourMults = {
Absolute: 1,
Explosive: oppArmour.explosive.damageMultiplier,
Kinetic: oppArmour.kinetic.damageMultiplier,
Thermic: oppArmour.thermal.damageMultiplier,
};
let rows = [];
for (let weapon of ship.getHardpoints()) {
const sdps = weapon.get('sustaineddamagepersecond');
const byRange = weapon.getRangeEffectiveness();
const weaponPortions = {
Absolute: weapon.get('absolutedamageportion'),
Explosive: weapon.get('explosivedamageportion'),
Kinetic: weapon.get('kineticdamageportion'),
Thermic: weapon.get('thermicdamageportion'),
};
const baseSdpsTooltip = objToTooltip(
translate,
mapValues(weaponPortions, (p) => formats.f1(sdps * p)),
);
let totalSEps = 0;
let totalSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
let shieldsSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
let armourSDpsObject = { 'absolute': 0, 'explosive': 0, 'kinetic': 0, 'thermal': 0 };
const bySys = oppShield.absolute.bySys;
const shieldResEfts = mergeWith(
clone(weaponPortions),
shieldMults,
(objV, srcV) => objV * srcV
);
const byShieldRes = sum(values(shieldResEfts));
const shieldsSdpsTooltip = objToTooltip(
translate,
mapValues(
shieldResEfts,
(mult) => formats.f1(byRange * mult * bySys * sdps),
),
);
const shieldsEftTooltip = objToTooltip(
translate,
{
range: formats.pct1(byRange),
resistance: formats.pct1(byShieldRes),
'power distributor': formats.pct1(bySys),
},
);
const shieldEft = byRange * byShieldRes * bySys;
const rows = [];
for (let i = 0; i < damage.length; i++) {
const weapon = damage[i];
const byHardness = weapon.getArmourEffectiveness();
const armourResEfts = mergeWith(
clone(weaponPortions),
armourMults,
(objV, srcV) => objV * srcV,
);
const byArmourRes = sum(values(armourResEfts));
const armourSdpsTooltip = objToTooltip(
translate,
mapValues(
armourResEfts,
(mult) => formats.f1(byRange * mult * byHardness * sdps)
),
);
const armourEftTooltip = objToTooltip(
translate,
{
range: formats.pct1(byRange),
resistance: formats.pct1(byArmourRes),
hardness: formats.pct1(byHardness),
},
);
const armourEft = byRange * byArmourRes * byHardness;
totalSEps += weapon.seps;
addSDps(totalSDpsObject, weapon.sdps.base);
addSDps(shieldsSDpsObject, weapon.sdps.shields);
addSDps(armourSDpsObject, weapon.sdps.armour);
const baseSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.base);
const effectivenessShieldsTooltipDetails = [];
effectivenessShieldsTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.shields.range)}</div>);
effectivenessShieldsTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.shields.resistance)}</div>);
effectivenessShieldsTooltipDetails.push(<div key='power distributor'>{translate('power distributor') + ' ' + formats.pct1(weapon.effectiveness.shields.sys)}</div>);
const effectiveShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
const effectivenessArmourTooltipDetails = [];
effectivenessArmourTooltipDetails.push(<div key='range'>{translate('range') + ' ' + formats.pct1(weapon.effectiveness.armour.range)}</div>);
effectivenessArmourTooltipDetails.push(<div key='resistance'>{translate('resistance') + ' ' + formats.pct1(weapon.effectiveness.armour.resistance)}</div>);
effectivenessArmourTooltipDetails.push(<div key='hardness'>{translate('hardness') + ' ' + formats.pct1(weapon.effectiveness.armour.hardness)}</div>);
const effectiveArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, weapon.sdps.armour);
rows.push(
<tr key={weapon.id}>
<td className='ri'>
{weapon.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{weapon.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{weapon.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{weapon.classRating} {translate(weapon.name)}
{weapon.engineering ? ' (' + weapon.engineering + ')' : null }
</td>
<td className='ri'><span onMouseOver={termtip.bind(null, baseSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.base.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessShieldsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.shields.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectiveArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.f1(weapon.sdps.armour.total)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, effectivenessArmourTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>{formats.pct1(weapon.effectiveness.armour.total)}</span></td>
</tr>);
const bp = weapon.getBlueprint();
const grade = weapon.getBlueprintGrade();
const exp = weapon.getExperimental();
let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`;
if (exp) {
bpTitle += `, ${translate(exp)}`;
}
rows.push({
slot: weapon.getSlot(),
mount: weapon.mount,
classRating: weapon.getClassRating(),
type: weapon.readMeta('type'),
bpTitle: bp ? ` (${bpTitle})` : null,
sdps,
baseSdpsTooltip,
shieldSdps: sdps * shieldEft,
shieldEft,
shieldsSdpsTooltip,
shieldsEftTooltip,
armourSdps: sdps * armourEft,
armourEft,
armourSdpsTooltip,
armourEftTooltip,
});
}
const { predicate, desc } = this.state;
rows = sortBy(rows, (row) => row[predicate]);
if (desc) {
reverse(rows);
}
const totalSDps = sumSDps(totalSDpsObject);
const totalSDpsTooltipDetails = getSDpsTooltip(translate, formats, totalSDpsObject);
const totalSDpsData = getSDpsData(translate, totalSDpsObject);
const sdpsTooltip = objToTooltip(
translate,
mapValues(portions, (p) => formats.f1(damage.sustained.dps * p)),
);
const sdpsPie = objToPie(
translate,
mapValues(portions, (p) => Math.round(damage.sustained.dps * p)),
);
const totalShieldsSDps = sumSDps(shieldsSDpsObject);
const totalShieldsSDpsTooltipDetails = getSDpsTooltip(translate, formats, shieldsSDpsObject);
const shieldsSDpsData = getSDpsData(translate, shieldsSDpsObject);
const shieldSdpsSrcs = mergeWith(
clone(portions),
shieldMults,
(objV, srcV) => damage.sustained.dps * oppShield.absolute.bySys *
damage.rangeMultiplier * objV * srcV,
);
const shieldsSdps = sum(values(shieldSdpsSrcs));
const shieldsSdpsTooltip = objToTooltip(
translate,
mapValues(shieldSdpsSrcs, (v) => formats.f1(v)),
);
const shieldsSdpsPie = objToPie(
translate,
mapValues(shieldSdpsSrcs, (v) => Math.round(v)),
);
const totalArmourSDps = sumSDps(armourSDpsObject);
const totalArmourSDpsTooltipDetails = getSDpsTooltip(translate, formats, armourSDpsObject);
const armourSDpsData = getSDpsData(translate, armourSDpsObject);
const armourSdpsSrcs = mergeWith(
clone(portions),
armourMults,
(objV, srcV) => damage.sustained.dps * damage.hardnessMultiplier *
damage.rangeMultiplier * objV * srcV,
);
const armourSdps = sum(values(armourSdpsSrcs));
const totalArmourSDpsTooltipDetails = objToTooltip(
translate,
mapValues(armourSdpsSrcs, (v) => formats.f1(v)),
);
const armourSDpsData = objToPie(
translate,
mapValues(armourSdpsSrcs, (v) => Math.round(v)),
);
const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, totalShieldsSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, totalArmourSDps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
const pd = ship.getPowerDistributor();
const timeToDrain = damage.sustained.timeToDrain[ship.getDistributorSettings().Wep];
// const timeToDepleteShields = Calc.timeToDeplete(opponentShields.total, shieldsSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
// const timeToDepleteArmour = Calc.timeToDeplete(opponentArmour.total, armourSdps, totalSEps, pd.getWeaponsCapacity(), pd.getWeaponsRechargeRate() * (wep / 4));
return (
<span id='offence'>
@@ -269,28 +252,75 @@ export default class Offence extends TranslatedComponent {
<table>
<thead>
<tr className='main'>
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'n')}>{translate('weapon')}</th>
<th rowSpan='2' className='sortable' onClick={sortOrder.bind(this, 'classRating')}>{translate('weapon')}</th>
<th colSpan='1'>{translate('overall')}</th>
<th colSpan='2'>{translate('opponent\'s shields')}</th>
<th colSpan='2'>{translate('opponent\'s armour')}</th>
</tr>
<tr>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')} onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'esdpss')}>{'sdps'}</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'es')}>{'eft'}</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'esdpsh')}>{'sdps'}</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')} onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'eh')}>{'eft'}</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'sdps')}>sdps</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_SHIELDS')}
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'shieldSdps')}>sdps</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_SHIELDS')}
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'shieldEft')}>eft</th>
<th className='lft sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVE_SDPS_ARMOUR')}
onMouseOut={tooltip.bind(null, null)}onClick={sortOrder.bind(this, 'armourSdps')}>sdps</th>
<th className='sortable' onMouseOver={termtip.bind(null, 'TT_EFFECTIVENESS_ARMOUR')}
onMouseOut={tooltip.bind(null, null)} onClick={sortOrder.bind(this, 'armourEft')}>eft</th>
</tr>
</thead>
<tbody>
{rows}
{rows.map((row) => (
<tr key={row.slot}>
<td className='ri'>
{row.mount == 'F' ? <span onMouseOver={termtip.bind(null, 'fixed')} onMouseOut={tooltip.bind(null, null)}><MountFixed className='icon'/></span> : null}
{row.mount == 'G' ? <span onMouseOver={termtip.bind(null, 'gimballed')} onMouseOut={tooltip.bind(null, null)}><MountGimballed /></span> : null}
{row.mount == 'T' ? <span onMouseOver={termtip.bind(null, 'turreted')} onMouseOut={tooltip.bind(null, null)}><MountTurret /></span> : null}
{row.classRating} {translate(row.type)}
{row.bpTitle}
</td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.baseSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.sdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.shieldsSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.shieldSdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.shieldsEftTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.pct1(row.shieldEft)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.armourSdpsTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.f1(row.armourSdps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, row.armourEftTooltip)}
onMouseOut={tooltip.bind(null, null)}
>{formats.pct1(row.armourEft)}</span></td>
</tr>
))}
{rows.length > 0 &&
<tr>
<td></td>
<td className='ri'><span onMouseOver={termtip.bind(null, totalSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalSDps)}</span></td>
<td className='ri'><span onMouseOver={termtip.bind(null, totalShieldsSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalShieldsSDps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, sdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
={formats.f1(damage.sustained.dps)}
</span>
</td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, shieldsSdpsTooltip)} onMouseOut={tooltip.bind(null, null)}>
={formats.f1(shieldsSdps)}
</span>
</td>
<td></td>
<td className='ri'><span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>={formats.f1(totalArmourSDps)}</span></td>
<td className='ri'>
<span onMouseOver={termtip.bind(null, totalArmourSDpsTooltipDetails)} onMouseOut={tooltip.bind(null, null)}>
={formats.f1(armourSdps)}
</span>
</td>
<td></td>
</tr>
}
@@ -299,22 +329,53 @@ export default class Offence extends TranslatedComponent {
</div>
<div className='group quarter'>
<h2>{translate('offence metrics')}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}<br/>{formats.f1(totalShieldsSDps)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>{timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}<br/>{formats.f1(totalArmourSDps)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))} onMouseOut={tooltip.bind(null, null)}>{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>{timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)}</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_DRAIN_WEP'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_TIME_TO_DRAIN_WEP')}<br/>
{timeToDrain === Infinity ? translate('never') : formats.time(timeToDrain)}
</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_SHIELDS'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_EFFECTIVE_SDPS_SHIELDS')}<br/>
{formats.f1(shieldsSdps)}
</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_SHIELDS'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_TIME_TO_REMOVE_SHIELDS')}<br/>
ToDo
{/* {timeToDepleteShields === Infinity ? translate('never') : formats.time(timeToDepleteShields)} */}
</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_EFFECTIVE_SDPS_ARMOUR'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_EFFECTIVE_SDPS_ARMOUR')}<br/>
{formats.f1(armourSdps)}
</h2>
<h2 onMouseOver={termtip.bind(null, translate('TT_TIME_TO_REMOVE_ARMOUR'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('PHRASE_TIME_TO_REMOVE_ARMOUR')}<br/>
ToDo
{/* {timeToDepleteArmour === Infinity ? translate('never') : formats.time(timeToDepleteArmour)} */}
</h2>
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('overall damage')}</h2>
<PieChart data={totalSDpsData} />
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_OVERALL_DAMAGE'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('overall damage')}
</h2>
<PieChart data={sdpsPie} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('shield damage sources')}</h2>
<PieChart data={shieldsSDpsData} />
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_SHIELD_DAMAGE'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('shield damage sources')}
</h2>
<PieChart data={shieldsSdpsPie} />
</div>
<div className='group quarter'>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))} onMouseOut={tooltip.bind(null, null)}>{translate('armour damage sources')}</h2>
<h2 onMouseOver={termtip.bind(null, translate('PHRASE_ARMOUR_DAMAGE'))}
onMouseOut={tooltip.bind(null, null)}>
{translate('armour damage sources')}
</h2>
<PieChart data={armourSDpsData} />
</div>
</span>);

View File

@@ -11,29 +11,24 @@ import Movement from './Movement';
import Offence from './Offence';
import Defence from './Defence';
import WeaponDamageChart from './WeaponDamageChart';
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 autoBind from 'auto-bind';
import { ShipProps } from 'ed-forge';
const { CARGO_CAPACITY, FUEL_CAPACITY } = ShipProps;
/**
* Outfitting subpages
*/
export default class OutfittingSubpages extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
code: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
buildName: PropTypes.string,
sys: PropTypes.number.isRequired,
eng: PropTypes.number.isRequired,
wep: PropTypes.number.isRequired,
cargo: PropTypes.number.isRequired,
fuel: PropTypes.number.isRequired,
boost: PropTypes.bool.isRequired,
engagementRange: PropTypes.number.isRequired,
opponent: PropTypes.object.isRequired,
opponentBuild: PropTypes.string,
opponentSys: PropTypes.number.isRequired,
opponentEng: PropTypes.number.isRequired,
opponentWep: PropTypes.number.isRequired,
};
/**
@@ -42,13 +37,17 @@ export default class OutfittingSubpages extends TranslatedComponent {
*/
constructor(props) {
super(props);
this._powerTab = this._powerTab.bind(this);
this._profilesTab = this._profilesTab.bind(this);
this._offenceTab = this._offenceTab.bind(this);
this._defenceTab = this._defenceTab.bind(this);
autoBind(this);
this.props.ship.setOpponent(this.props.ship);
this.state = {
boost: false,
cargo: props.ship.get(CARGO_CAPACITY),
fuel: props.ship.get(FUEL_CAPACITY),
pips: props.ship.getDistributorSettingsObject(),
tab: Persist.getOutfittingTab() || 'power',
engagementRange: 1000,
opponent: this.props.ship,
};
}
@@ -57,128 +56,114 @@ export default class OutfittingSubpages extends TranslatedComponent {
* @param {string} tab Tab name
*/
_showTab(tab) {
Persist.setOutfittingTab(tab);
this.setState({ tab });
}
/**
* Render the power tab
* @return {React.Component} Tab contents
*/
_powerTab() {
let { ship, buildName, code, onChange } = this.props;
Persist.setOutfittingTab('power');
const powerMarker = `${ship.toString()}`;
const costMarker = `${ship.toString().split('.')[0]}`;
return <div>
<PowerManagement ship={ship} code={powerMarker} onChange={onChange} />
<CostSection ship={ship} buildName={buildName} code={costMarker} />
</div>;
}
/**
* Render the profiles tab
* @return {React.Component} Tab contents
*/
_profilesTab() {
const { ship, opponent, cargo, fuel, eng, boost, engagementRange, opponentSys } = this.props;
const { translate } = this.context.language;
let realBoost = boost && ship.canBoost(cargo, fuel);
Persist.setOutfittingTab('profiles');
const engineProfileMarker = `${ship.toString()}:${cargo}:${fuel}:${eng}:${realBoost}`;
const fsdProfileMarker = `${ship.toString()}:${cargo}:${fuel}`;
const movementMarker = `${ship.topSpeed}:${ship.pitch}:${ship.roll}:${ship.yaw}:${ship.canBoost(cargo, fuel)}`;
const damageMarker = `${ship.toString()}:${opponent.toString()}:${engagementRange}:${opponentSys}`;
return <div>
<div className='group third'>
<h1>{translate('engine profile')}</h1>
<EngineProfile ship={ship} marker={engineProfileMarker} fuel={fuel} cargo={cargo} eng={eng} boost={realBoost} />
</div>
<div className='group third'>
<h1>{translate('fsd profile')}</h1>
<FSDProfile ship={ship} marker={fsdProfileMarker} fuel={fuel} cargo={cargo} />
</div>
<div className='group third'>
<h1>{translate('movement profile')}</h1>
<Movement marker={movementMarker} ship={ship} boost={boost} eng={eng} cargo={cargo} fuel={fuel} />
</div>
<div className='group half'>
<h1>{translate('damage to opponent\'s shields')}</h1>
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={false} engagementRange={engagementRange} />
</div>
<div className='group half'>
<h1>{translate('damage to opponent\'s hull')}</h1>
<WeaponDamageChart marker={damageMarker} ship={ship} opponent={opponent} opponentSys={opponentSys} hull={true} engagementRange={engagementRange} />
</div>
</div>;
}
/**
* Render the offence tab
* @return {React.Component} Tab contents
*/
_offenceTab() {
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentSys } = this.props;
Persist.setOutfittingTab('offence');
const marker = `${ship.toString()}${opponent.toString()}${opponentBuild}${engagementRange}${opponentSys}`;
return <div>
<Offence marker={marker} ship={ship} opponent={opponent} wep={wep} opponentSys={opponentSys} engagementrange={engagementRange}/>
</div>;
}
/**
* Render the defence tab
* @return {React.Component} Tab contents
*/
_defenceTab() {
const { ship, sys, eng, wep, cargo, fuel, boost, engagementRange, opponent, opponentBuild, opponentWep } = this.props;
Persist.setOutfittingTab('defence');
const marker = `${ship.toString()}${opponent.toString()}{opponentBuild}${engagementRange}${opponentWep}`;
return <div>
<Defence marker={marker} ship={ship} opponent={opponent} sys={sys} opponentWep={opponentWep} engagementrange={engagementRange}/>
</div>;
}
/**
* Render the section
* @return {React.Component} Contents
*/
render() {
const tab = this.state.tab;
const translate = this.context.language.translate;
let tabSection;
switch (tab) {
case 'power': tabSection = this._powerTab(); break;
case 'profiles': tabSection = this._profilesTab(); break;
case 'offence': tabSection = this._offenceTab(); break;
case 'defence': tabSection = this._defenceTab(); break;
}
const { buildName, code, ship } = this.props;
const { boost, cargo, fuel, pips, tab, engagementRange, opponent } = this.state;
const { translate } = this.context.language;
const cargoCapacity = ship.get(CARGO_CAPACITY);
const showCargoSlider = cargoCapacity > 0;
return (
<div>
{/* Control of ship and opponent */}
<div className="group quarter">
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
{translate('ship control')}
</h2>
<Boost boost={boost} onChange={(boost) => this.setState({ boost })} />
</div>
<div className="group quarter">
<h2 style={{ verticalAlign: 'middle', textAlign: 'center' }}>
{translate('opponent')}
</h2>
<ShipPicker ship={ship} onChange={(opponent) => this.setState({ opponent })} />
</div>
<div className={cn('group', { quarter: showCargoSlider, half: !showCargoSlider })}>
<Fuel fuelCapacity={ship.get(FUEL_CAPACITY)} fuel={fuel}
onChange={(fuel) => this.setState({ fuel })} />
</div>
{showCargoSlider ?
<div className="group quarter">
<Cargo cargoCapacity={cargoCapacity} cargo={cargo}
onChange={(cargo) => this.setState({ cargo })} />
</div> : null}
<div className="group half">
<Pips ship={ship} pips={pips} onChange={(pips) => this.setState({ pips })} />
</div>
<div className="group half">
<EngagementRange ship={ship} engagementRange={engagementRange}
onChange={(engagementRange) => this.setState({ engagementRange })} />
</div>
<div className='group full' style={{ minHeight: '1000px' }}>
<table className='tabs'>
{/* Select tab section */}
<thead>
<tr>
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })} onClick={this._showTab.bind(this, 'power')} >{translate('power and costs')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })} onClick={this._showTab.bind(this, 'profiles')} >{translate('profiles')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })} onClick={this._showTab.bind(this, 'offence')} >{translate('offence')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })} onClick={this._showTab.bind(this, 'defence')} >{translate('tab_defence')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'power' })}
onClick={this._showTab.bind(this, 'power')}>
{translate('power and costs')}
</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'profiles' })}
onClick={this._showTab.bind(this, 'profiles')}>
{translate('profiles')}</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'offence' })}
onClick={this._showTab.bind(this, 'offence')}>
{translate('offence')}
</th>
<th style={{ width:'25%' }} className={cn({ active: tab == 'defence' })}
onClick={this._showTab.bind(this, 'defence')}>
{translate('tab_defence')}
</th>
</tr>
</thead>
</table>
{tabSection}
{/* Show selected tab */}
{tab == 'power' ?
<div>
<PowerManagement ship={ship} code={code} />
<CostSection ship={ship} buildName={buildName} code={code} />
</div> : null}
{tab == 'profiles' ?
<div>
<div className='group third'>
<h1>{translate('engine profile')}</h1>
<EngineProfile code={code} ship={ship} fuel={fuel} cargo={cargo} pips={pips} boost={boost} />
</div>
<div className='group third'>
<h1>{translate('fsd profile')}</h1>
<FSDProfile code={code} ship={ship} fuel={fuel} cargo={cargo} />
</div>
<div className='group third'>
<h1>{translate('movement profile')}</h1>
<Movement code={code} ship={ship} boost={boost} pips={pips} />
</div>
<div className='group third'>
<h1>{translate('damage to opponent\'s shields')}</h1>
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getShield()} engagementRange={engagementRange} />
</div>
<div className='group third'>
<h1>{translate('damage to opponent\'s hull')}</h1>
<WeaponDamageChart code={code} ship={ship} opponentDefence={opponent.getArmour()} engagementRange={engagementRange} />
</div>
</div> : null}
{tab == 'offence' ?
<div>
<Offence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
</div> : null}
{tab == 'defence' ?
<div>
<Defence code={code} ship={ship} opponent={opponent} engagementRange={engagementRange} />
</div> : null}
</div>
</div>
);
}

View File

@@ -10,7 +10,6 @@ const LABEL_COLOUR = '#000000';
* A pie chart
*/
export default class PieChart extends Component {
static propTypes = {
data : PropTypes.array.isRequired
};

View File

@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import { Pip } from './SvgIcons';
import { autoBind } from 'react-extras';
import { Ship } from 'ed-forge';
/**
* Pips displays SYS/ENG/WEP pips and allows users to change them with key presses by clicking on the relevant area.
@@ -10,13 +11,9 @@ import { autoBind } from 'react-extras';
*/
export default class Pips extends TranslatedComponent {
static propTypes = {
sys: PropTypes.number.isRequired,
eng: PropTypes.number.isRequired,
wep: PropTypes.number.isRequired,
mcSys: PropTypes.number.isRequired,
mcEng: PropTypes.number.isRequired,
mcWep: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired
ship: PropTypes.instanceOf(Ship).isRequired,
pips: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
};
/**
@@ -27,6 +24,12 @@ export default class Pips extends TranslatedComponent {
constructor(props, context) {
super(props);
autoBind(this);
const { ship } = props;
this._incSys = this._change(ship.incSys);
this._incEng = this._change(ship.incEng);
this._incWep = this._change(ship.incWep);
this._reset = this._change(ship.pipsReset);
}
/**
@@ -71,153 +74,42 @@ export default class Pips extends TranslatedComponent {
}
/**
* Reset the capacitor
*/
_reset(isMc) {
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
if (isMc) {
if (mcSys || mcEng || mcWep) {
sys -= mcSys;
eng -= mcEng;
wep -= mcWep;
this.props.onChange(sys, eng, wep, 0, 0, 0);
}
} else if (sys != 2 || eng != 2 || wep != 2) {
sys = eng = wep = 2;
this.props.onChange(sys + mcSys, eng + mcEng, wep + mcWep, mcSys, mcEng, mcWep);
}
}
/**
* Increment the SYS capacitor
*/
_incSys() {
this._inc('sys', false);
}
/**
* Increment the ENG capacitor
*/
_incEng() {
this._inc('eng', false);
}
/**
* Increment the WEP capacitor
*/
_incWep() {
this._inc('wep', false);
}
_wrapMcClick(key) {
return (event) => {
event.stopPropagation();
event.preventDefault();
if (key == 'rst') {
this._reset(true);
} else {
this._inc(key, true);
}
};
}
/**
* Increases a given capacitor
* @param {String} key Pip name to increase (one of 'sys', 'eng', 'wep')
* Creates a function that handles pip assignment and call `onChance`.
* @param {String} cb Callback that handles the actual pip assignment
* @param {Boolean} isMc True when increase is by multi crew
* @returns {Function} Function that handles pip assigment
*/
_inc(key, isMc) {
if (!['sys', 'eng', 'wep'].includes(key)) {
return;
}
let { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
let mc = key == 'sys' ? mcSys : (key == 'eng' ? mcEng : mcWep);
let pips = this.props[key] - mc;
let other1 = key == 'sys' ? eng - mcEng : sys - mcSys;
let other2 = key == 'wep' ? eng - mcEng : wep - mcWep;
const required = Math.min(1, 4 - mc - pips);
if (isMc) {
// We can only set full pips in multi-crew also we can only set two pips
if (required > 0.5 && mcSys + mcEng + mcWep < 2) {
if (key == 'sys') {
mcSys += 1;
} else if (key == 'eng') {
mcEng += 1;
} else {
mcWep += 1;
}
}
} else if (required > 0) {
if (required == 0.5) {
// Take from whichever is larger
if (other1 > other2) {
other1 -= 0.5;
} else {
other2 -= 0.5;
}
pips += 0.5;
} else {
// Required is 1 - take from both if possible
if (other1 == 0) {
other2 -= 1;
} else if (other2 == 0) {
other1 -= 1;
} else {
other1 -= 0.5;
other2 -= 0.5;
}
pips += 1;
}
}
sys = mcSys + (key == 'sys' ? pips : other1);
eng = mcEng + (key == 'eng' ? pips : (key == 'sys' ? other1 : other2));
wep = mcWep + (key == 'wep' ? pips : other2);
this.props.onChange(sys, eng, wep, mcSys, mcEng, mcWep);
_change(cb, isMc) {
return () => {
cb(isMc);
this.props.onChange(this.props.ship.getDistributorSettingsObject());
};
}
/**
* Set up the rendering for pips
* @param {Number} sys the SYS pips
* @param {Number} eng the ENG pips
* @param {Number} wep the WEP pips
* @param {Number} mcSys SYS pips from multi-crew
* @param {Number} mcEng ENG pips from multi-crew
* @param {Number} mcWep WEP pips from multi-crew
* @returns {Object} Object containing the rendering for the pips
*/
_renderPips(sys, eng, wep, mcSys, mcEng, mcWep) {
_renderPips() {
const pipsSvg = {
SYS: [],
ENG: [],
WEP: [],
Sys: [],
Eng: [],
Wep: [],
};
// Multi-crew pipsSettings actually are included in the overall pip count therefore
// we can consider [0, sys - mcSys] as normal pipsSettings whilst [sys - mcSys, sys]
// are the multi-crew pipsSettings in what follows.
let pipsSettings = {
SYS: [sys, mcSys],
ENG: [eng, mcEng],
WEP: [wep, mcWep],
};
for (let pipName in pipsSettings) {
let [pips, mcPips] = pipsSettings[pipName];
for (let i = 0; i < Math.floor(pips - mcPips); i++) {
pipsSvg[pipName].push(<Pip key={i} className='full' />);
for (let k in this.props.pips) {
let { base, mc } = this.props.pips[k];
for (let i = 0; i < Math.floor(base); i++) {
pipsSvg[k].push(<Pip key={i} className='full' />);
}
if (pips > Math.floor(pips)) {
pipsSvg[pipName].push(<Pip className='half' key={'half'} />);
if (base > Math.floor(base)) {
pipsSvg[k].push(<Pip className='half' key={'half'} />);
}
for (let i = pips - mcPips; i < Math.floor(pips); i++) {
pipsSvg[pipName].push(<Pip key={i} className='mc' />);
for (let i = 0; i < mc; i++) {
pipsSvg[k].push(<Pip key={base + i} className='mc' />);
}
for (let i = Math.floor(pips + 0.5); i < 4; i++) {
pipsSvg[pipName].push(<Pip className='empty' key={i} />);
for (let i = Math.ceil(base + mc); i < 4; i++) {
pipsSvg[k].push(<Pip className='empty' key={i} />);
}
}
@@ -229,11 +121,10 @@ export default class Pips extends TranslatedComponent {
* @return {React.Component} contents
*/
render() {
const { tooltip, termtip } = this.context;
const { formats, translate, units } = this.context.language;
const { sys, eng, wep, mcSys, mcEng, mcWep } = this.props;
const { ship } = this.props;
const { translate } = this.context.language;
const pipsSvg = this._renderPips(sys, eng, wep, mcSys, mcEng, mcWep);
const pipsSvg = this._renderPips();
return (
<span id='pips'>
<table>
@@ -241,38 +132,40 @@ export default class Pips extends TranslatedComponent {
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td className='clickable' onClick={() => this._inc('eng')}
onContextMenu={this._wrapMcClick('eng')}>{pipsSvg['ENG']}</td>
<td className='clickable' onClick={this._incEng}>{pipsSvg.Eng}</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td className='clickable' onClick={this._incSys}
onContextMenu={this._wrapMcClick('sys')}>{pipsSvg['SYS']}</td>
<td className='clickable' onClick={this._incEng}
onContextMenu={this._wrapMcClick('eng')}>{translate('ENG')}</td>
<td className='clickable' onClick={this._incWep}
onContextMenu={this._wrapMcClick('wep')}>{pipsSvg['WEP']}</td>
<td className='clickable' onClick={this._incSys}>{pipsSvg.Sys}</td>
<td className='clickable' onClick={this._incEng}>
{translate('ENG')}
</td>
<td className='clickable' onClick={this._incWep}>{pipsSvg.Wep}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td className='clickable' onClick={this._incSys}
onContextMenu={this._wrapMcClick('sys')}>{translate('SYS')}</td>
<td className='clickable' onClick={this._reset.bind(this, false)}>
<td className='clickable' onClick={this._incSys}>
{translate('SYS')}
</td>
<td className='clickable' onClick={this._reset}>
{translate('RST')}
</td>
<td className='clickable' onClick={this._incWep}
onContextMenu={this._wrapMcClick('wep')}>{translate('WEP')}</td>
<td className='clickable' onClick={this._incWep}>
{translate('WEP')}
</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>&nbsp;</td>
<td className='clickable secondary' onClick={this._wrapMcClick('rst')}
onMouseEnter={termtip.bind(null, 'PHRASE_MULTI_CREW_CAPACITOR_POINTS')}
onMouseLeave={tooltip.bind(null, null)}>
{translate('RST')}
<td className='clickable' onClick={this._change(ship.incSys, true)}>
<Pip className='mc' />
</td>
<td className='clickable' onClick={this._change(ship.incEng, true)}>
<Pip className='mc' />
</td>
<td className='clickable' onClick={this._change(ship.incWep, true)}>
<Pip className='mc' />
</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>

View File

@@ -4,27 +4,25 @@ import * as d3 from 'd3';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { Ship } from 'ed-forge';
import { POWER_METRICS } from 'ed-forge/lib/ship-stats';
import autoBind from 'auto-bind';
/**
* Round to avoid floating point precision errors
* Get the band-class.
* @param {Boolean} selected Band selected
* @param {number} sum Band power sum
* @param {number} avail Total available power
* @param {Number} relDraw Relative amount of power drawn by this band and
* all prior
* @return {string} CSS Class name
*/
function getClass(selected, sum, avail) {
return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary';
}
/**
* Get the # label for a Priority band
* @param {number} val Priority Band Watt value
* @param {number} index Priority Band index
* @param {Function} wattScale Watt Scale function
* @return {number} label / text
*/
function bandText(val, index, wattScale) {
return (val > 0 && wattScale(val) > 13) ? index + 1 : null;
function getClass(selected, relDraw) {
if (selected) {
return 'secondary';
} else if (relDraw >= 1) {
return 'warning';
} else {
return 'primary';
}
}
/**
@@ -33,10 +31,9 @@ function bandText(val, index, wattScale) {
*/
export default class PowerBands extends TranslatedComponent {
static propTypes = {
bands: PropTypes.array.isRequired,
available: PropTypes.number.isRequired,
ship: PropTypes.instanceOf(Ship).isRequired,
code: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
code: PropTypes.string,
};
/**
@@ -46,20 +43,16 @@ export default class PowerBands extends TranslatedComponent {
*/
constructor(props, context) {
super(props);
autoBind(this);
this.wattScale = d3.scaleLinear();
this.pctScale = d3.scaleLinear().domain([0, 1]);
this.wattAxis = d3.axisTop(this.wattScale).tickSizeOuter(0).tickFormat(context.language.formats.r2);
this.pctAxis = d3.axisBottom(this.pctScale).tickSizeOuter(0).tickFormat(context.language.formats.rPct);
this._updateDimensions = this._updateDimensions.bind(this);
this._updateScales = this._updateScales.bind(this);
this._selectNone = this._selectNone.bind(this);
this._hidetip = () => this.context.tooltip();
let maxBand = props.bands[props.bands.length - 1];
this.profile = props.ship.getMetrics(POWER_METRICS);
this.state = {
maxPwr: Math.max(props.available, maxBand.retractedSum, maxBand.deployedSum),
ret: {},
dep: {}
};
@@ -83,8 +76,6 @@ export default class PowerBands extends TranslatedComponent {
let mRight = Math.round(140 * scale);
let innerWidth = props.width - mLeft - mRight;
this._updateScales(innerWidth, this.state.maxPwr, props.available);
this.setState({
barHeight,
innerHeight,
@@ -140,41 +131,67 @@ export default class PowerBands extends TranslatedComponent {
this.setState({ dep: Object.assign({}, dep) });
}
/**
* Update scale
* @param {number} innerWidth SVG innerwidth
* @param {number} maxPwr Maximum power level MJ (deployed or available)
* @param {number} available Available power MJ
*/
_updateScales(innerWidth, maxPwr, available) {
this.wattScale.range([0, innerWidth]).domain([0, maxPwr]).clamp(true);
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / available]).clamp(true);
}
/**
* Update state based on property and context changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next context
*/
componentWillReceiveProps(nextProps, nextContext) {
let { innerWidth, maxPwr } = this.state;
let { language, sizeRatio } = this.context;
let maxBand = nextProps.bands[nextProps.bands.length - 1];
let nextMaxPwr = Math.max(nextProps.available, maxBand.retractedSum, maxBand.deployedSum);
if (language !== nextContext.language) {
this.wattAxis.tickFormat(nextContext.language.formats.r2);
this.pctAxis.tickFormat(nextContext.language.formats.rPct);
}
if (maxPwr != nextMaxPwr) { // Update Axes if max power has changed
this._updateScales(innerWidth, nextMaxPwr, nextProps.available);
this.setState({ maxPwr: nextMaxPwr });
} else if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
if (nextProps.width != this.props.width || sizeRatio != nextContext.sizeRatio) {
this._updateDimensions(nextProps, nextContext.sizeRatio);
}
}
/**
* Assemble bands for relative consumption array.
* @param {Number[]} consumed Array of relative-consumption numbers
* @param {object} selected Object mapping selected bands to 1
* @param {Number} yOffset Offset in y-direction of the bar
* @param {Function} onClick onClick callback
* @returns {React.Component} Bands
*/
_consumedToBands(consumed, selected, yOffset, onClick) {
const { state, wattScale } = this;
const bands = [];
let consumesPrev = 0;
for (let i = 0; i < consumed.length; i++) {
consumesPrev = consumed[i - 1] || consumesPrev;
const consumes = consumed[i];
if (!consumes) {
continue;
}
bands.push(<rect
key={'b' + i}
width={Math.ceil(Math.max(wattScale(consumes - consumesPrev), 0))}
height={state.barHeight}
x={wattScale(consumesPrev)}
y={yOffset + 1}
onClick={onClick.bind(this, i)}
className={getClass(selected[i], consumes)}
/>);
bands.push(<text
key={'t' + i}
dy='0.5em'
textAnchor='middle'
height={state.barHeight}
x={wattScale(consumesPrev) + (wattScale(consumes - consumesPrev) / 2)}
y={yOffset + (state.barHeight / 2)}
onClick={onClick.bind(this, i)}
className='primary-bg'>{i + 1}</text>
);
}
return bands;
}
/**
* Render the power bands
* @return {React.Component} Power bands
@@ -184,78 +201,27 @@ export default class PowerBands extends TranslatedComponent {
return null;
}
let { wattScale, pctScale, context, props, state } = this;
let { pctScale, context, props, state } = this;
let { translate, formats } = context.language;
let { f2, pct1 } = formats; // wattFmt, pctFmt
let { available, bands } = props;
let { innerWidth, ret, dep } = state;
let pwrWarningClass = cn('threshold', { exceeded: bands[0].retractedSum > available * 0.4 });
let deployed = [];
let retracted = [];
let { ship } = props;
let { innerWidth, ret, dep, barHeight } = state;
let {
consumed, generated, relativeConsumed, relativeConsumedRetracted
} = ship.getMetrics(POWER_METRICS);
let maxPwr = Math.max(consumed, generated);
let retSum = relativeConsumedRetracted[relativeConsumedRetracted.length - 1];
let depSum = relativeConsumed[relativeConsumed.length - 1];
this.wattScale.range([0, innerWidth]).domain([0, 1]).clamp(true);
this.pctScale.range([0, innerWidth]).domain([0, maxPwr / generated]).clamp(true);
let pwrWarningClass = cn('threshold', { exceeded: retSum > generated * 0.4 });
let retracted = this._consumedToBands(relativeConsumedRetracted, ret, 0, this._selectRet);
let deployed = this._consumedToBands(relativeConsumed, dep, barHeight, this._selectDep);
let retSelected = Object.keys(ret).length > 0;
let depSelected = Object.keys(dep).length > 0;
let retSum = 0;
let depSum = 0;
for (let i = 0; i < bands.length; i++) {
let b = bands[i];
retSum += (!retSelected || ret[i]) ? b.retracted : 0;
depSum += (!depSelected || dep[i]) ? b.deployed + b.retracted : 0;
if (b.retracted > 0) {
let retLbl = bandText(b.retracted, i, wattScale);
retracted.push(<rect
key={'rB' + i}
width={Math.ceil(Math.max(wattScale(b.retracted), 0))}
height={state.barHeight}
x={Math.floor(Math.max(wattScale(b.retractedSum) - wattScale(b.retracted), 0))}
y={1}
onClick={this._selectRet.bind(this, i)}
className={getClass(ret[i], b.retractedSum, available)}
/>);
if (retLbl) {
retracted.push(<text
key={'rT' + i}
dy='0.5em'
textAnchor='middle'
height={state.barHeight}
x={wattScale(b.retractedSum) - (wattScale(b.retracted) / 2)}
y={state.retY}
onClick={this._selectRet.bind(this, i)}
className='primary-bg'>{retLbl}</text>
);
}
}
if (b.retracted > 0 || b.deployed > 0) {
let depLbl = bandText(b.deployed + b.retracted, i, wattScale);
deployed.push(<rect
key={'dB' + i}
width={Math.ceil(Math.max(wattScale(b.deployed + b.retracted), 0))}
height={state.barHeight}
x={Math.floor(Math.max(wattScale(b.deployedSum) - wattScale(b.retracted) - wattScale(b.deployed), 0))}
y={state.barHeight + 1}
onClick={this._selectDep.bind(this, i)}
className={getClass(dep[i], b.deployedSum, available)}
/>);
if (depLbl) {
deployed.push(<text
key={'dT' + i}
dy='0.5em'
textAnchor='middle'
height={state.barHeight}
x={wattScale(b.deployedSum) - ((wattScale(b.retracted) + wattScale(b.deployed)) / 2)}
y={state.depY}
onClick={this._selectDep.bind(this, i)}
className='primary-bg'>{depLbl}</text>
);
}
}
}
return (
<svg style={{ marginTop: '1em', width: '100%', height: state.height }} onContextMenu={wrapCtxMenu(this._selectNone)}>
@@ -271,8 +237,8 @@ export default class PowerBands extends TranslatedComponent {
<line x1={pctScale(0.4)} x2={pctScale(0.4)} y1='0' y2={state.innerHeight} className={pwrWarningClass} />
<text dy='0.5em' x='-3' y={state.retY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'retracted')} onMouseLeave={this._hidetip}>{translate('ret')}</text>
<text dy='0.5em' x='-3' y={state.depY} className='primary upp' textAnchor='end' onMouseOver={this.context.termtip.bind(null, 'deployed', { orientation: 's', cap: 1 })} onMouseLeave={this._hidetip}>{translate('dep')}</text>
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, available)}>{f2(Math.max(0, retSum))} ({pct1(Math.max(0, retSum / available))})</text>
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, available)}>{f2(Math.max(0, depSum))} ({pct1(Math.max(0, depSum / available))})</text>
<text dy='0.5em' x={innerWidth + 5} y={state.retY} className={getClass(retSelected, retSum, generated)}>{f2(Math.max(0, retSum * generated))} ({pct1(Math.max(0, retSum))})</text>
<text dy='0.5em' x={innerWidth + 5} y={state.depY} className={getClass(depSelected, depSum, generated)}>{f2(Math.max(0, depSum * generated))} ({pct1(Math.max(0, depSum))})</text>
</g>
</svg>
);

View File

@@ -3,24 +3,49 @@ import PropTypes from 'prop-types';
import cn from 'classnames';
import TranslatedComponent from './TranslatedComponent';
import PowerBands from './PowerBands';
import { slotName, slotComparator } from '../utils/SlotFunctions';
import { Power, NoPower } from './SvgIcons';
import autoBind from 'auto-bind';
import { Ship, Module } from 'ed-forge';
const POWER = [
null,
null,
<NoPower className='icon warning' />,
<Power className='secondary-disabled' />
];
/**
* Makes a comparison based on the order `false < undefined < true` (fut) and
* maps it to `[-1, 0, 1]`.
* @param {boolean} a Bool or undefined
* @param {boolean} b Bool or undefined
* @returns {number} Comparison
*/
function futComp(a, b) {
switch (a) {
case false: return (b === false ? 0 : 1);
// The next else-expression maps false to -1 and true to 1
case undefined: return (b === undefined ? 0 : 2 * Number(b) - 1);
case true: return (b === true ? 0 : -1);
}
}
/**
* Get the enabled-icon.
* @param {boolean} enabled Is the module enabled?
* @returns {React.Component} Enabled icon.
*/
function getPowerIcon(enabled) {
if (enabled === undefined) {
return null;
}
if (enabled) {
return <Power className='secondary-disabled' />;
} else {
return <NoPower className='icon warning' />;
}
}
/**
* Power Management Section
*/
export default class PowerManagement extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
ship: PropTypes.instanceOf(Ship).isRequired,
code: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
};
/**
@@ -29,19 +54,17 @@ export default class PowerManagement extends TranslatedComponent {
*/
constructor(props) {
super(props);
this._renderPowerRows = this._renderPowerRows.bind(this);
this._updateWidth = this._updateWidth.bind(this);
this._sort = this._sort.bind(this);
autoBind(this);
this.state = {
predicate: 'pwr',
desc: false,
desc: true,
width: 0
};
}
/**
* Set the sort order and sort
* Set the sort order
* @param {string} predicate Sort predicate
*/
_sortOrder(predicate) {
@@ -53,50 +76,51 @@ export default class PowerManagement extends TranslatedComponent {
desc = true;
}
this._sort(this.props.ship, predicate, desc);
this.setState({ predicate, desc });
}
/**
* Sorts the power list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort order descending
* @param {Module[]} modules Modules to sort
* @returns {Module[]} Sorted modules
*/
_sort(ship, predicate, desc) {
let powerList = ship.powerList;
let comp = slotComparator.bind(null, this.context.language.translate);
_sortAndFilter(modules) {
modules = modules.filter((m) => m.get('powerdraw') >= 0);
let { translate } = this.context.language;
const { predicate, desc } = this.state;
let comp;
switch (predicate) {
case 'n': comp = comp(null, desc); break;
case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
case 'pri': comp = comp((a, b) => a.priority - b.priority, desc); break;
case 'pwr': comp = comp((a, b) => a.m.getPowerUsage() - b.m.getPowerUsage(), desc); break;
case 'r': comp = comp((a, b) => ship.getSlotStatus(a) - ship.getSlotStatus(b), desc); break;
case 'd': comp = comp((a, b) => ship.getSlotStatus(a, true) - ship.getSlotStatus(b, true), desc); break;
case 'n': comp = (a, b) => translate(a.readMeta('type')).localeCompare(
translate(b.readMeta('type'))
); break;
// case 't': comp = comp((a, b) => a.type.localeCompare(b.type), desc); break;
case 'pri': comp = (a, b) => a.getPowerPriority() - b.getPowerPriority(); break;
case 'pwr': comp = (a, b) => a.get('powerdraw') - b.get('powerdraw'); break;
case 'r': comp = (a, b) => futComp(a.isPowered().retracted, b.isPowered().retracted); break;
case 'd': comp = (a, b) => futComp(a.isPowered().deployed, b.isPowered().deployed); break;
}
powerList.sort(comp);
modules.sort(comp);
if (desc) {
modules.reverse();
}
return modules;
}
/**
* Update slot priority
* @param {Object} slot Slot model
* @param {number} inc increment / decrement
* Creates a callback that changes the power priority for the given module
* based on the given delta.
* @param {Module} m Module to set the priority for
* @param {Number} delta Delta to set
* @returns {Function} Callback
*/
_priority(slot, inc) {
if (this.props.ship.setSlotPriority(slot, slot.priority + inc)) {
this.props.onChange();
_prioCb(m, delta) {
return () => {
const prio = m.getPowerPriority();
const newPrio = Math.max(0, prio + delta);
if (0 <= newPrio) {
m.setPowerPriority(newPrio);
}
}
/**
* Toggle slot active/inactive
* @param {Object} slot Slot model
*/
_toggleEnabled(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
};
}
/**
@@ -110,37 +134,36 @@ export default class PowerManagement extends TranslatedComponent {
_renderPowerRows(ship, translate, pwr, pct) {
let powerRows = [];
for (let i = 0, l = ship.powerList.length; i < l; i++) {
let slot = ship.powerList[i];
if (slot.m && slot.m.getPowerUsage() > 0) {
let m = slot.m;
let toggleEnabled = this._toggleEnabled.bind(this, slot);
let modules = this._sortAndFilter(ship.getModules());
for (let m of modules) {
let retractedElem = null, deployedElem = null;
if (slot.enabled) {
retractedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, false)]}</td>;
deployedElem = <td className='ptr upp' onClick={toggleEnabled}>{POWER[ship.getSlotStatus(slot, true)]}</td>;
const flipEnabled = () => m.setEnabled();
if (m.isEnabled()) {
let powered = m.isPowered();
retractedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.retracted)}</td>;
deployedElem = <td className='ptr upp' onClick={flipEnabled}>{getPowerIcon(powered.deployed)}</td>;
} else {
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{translate('disabled')}</td>;
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={flipEnabled}>{translate('disabled')}</td>;
}
powerRows.push(<tr key={i} className={cn('highlight', { disabled: !slot.enabled })}>
<td className='ptr' style={{ width: '1em' }} onClick={toggleEnabled}>{m.class + m.rating}</td>
<td className='ptr le shorten cap' onClick={toggleEnabled}>{slotName(translate, slot)}</td>
<td className='ptr' onClick={toggleEnabled}><u>{translate(slot.type)}</u></td>
const slot = m.getSlot();
powerRows.push(<tr key={slot} className={cn('highlight', { disabled: !m.isEnabled() })}>
<td className='ptr' style={{ width: '1em' }} onClick={flipEnabled}>{String(m.getClass()) + m.getRating()}</td>
<td className='ptr le shorten cap' onClick={flipEnabled}>{translate(m.readMeta('type'))}</td>
{/* <td className='ptr' onClick={flipEnabled}><u>{translate(slot.type)}</u></td> */}
<td>
<span className='flip ptr btn' onClick={this._priority.bind(this, slot, -1)}>&#9658;</span>
{' ' + (slot.priority + 1) + ' '}
<span className='ptr btn' onClick={this._priority.bind(this, slot, 1)}>&#9658;</span>
<span className='flip ptr btn' onClick={this._prioCb(m, -1)}>&#9658;</span>
{' ' + (m.getPowerPriority() + 1) + ' '}
<span className='ptr btn' onClick={this._prioCb(m, 1)}>&#9658;</span>
</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={flipEnabled}>{pwr(m.get('powerdraw'))}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={flipEnabled}>
<u>{pct(m.get('powerdraw') / ship.getPowerPlant().get('powercapacity'))}</u>
</td>
<td className='ri ptr' style={{ width: '3.25em' }} onClick={toggleEnabled}>{pwr(m.getPowerUsage())}</td>
<td className='ri ptr' style={{ width: '3em' }} onClick={toggleEnabled}><u>{pct(m.getPowerUsage() / ship.powerAvailable)}</u></td>
{retractedElem}
{deployedElem}
</tr>);
}
}
return powerRows;
}
@@ -155,7 +178,6 @@ export default class PowerManagement extends TranslatedComponent {
* Add listeners when about to mount and sort power list
*/
componentWillMount() {
this._sort(this.props.ship, this.state.predicate, this.state.desc);
this.resizeListener = this.context.onWindowResize(this._updateWidth);
}
@@ -166,17 +188,6 @@ export default class PowerManagement extends TranslatedComponent {
this._updateWidth();
}
/**
* Sort power list if the ship instance has changed
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextState Incoming/Next state
*/
componentWillUpdate(nextProps, nextState) {
if (this.props.ship != nextProps.ship) {
this._sort(nextProps.ship, nextState.predicate, nextState.desc);
}
}
/**
* Remove listeners on unmount
*/
@@ -191,39 +202,38 @@ export default class PowerManagement extends TranslatedComponent {
render() {
let { ship, code } = this.props;
let { translate, formats } = this.context.language;
let pwr = formats.f2;
let pp = ship.standard[0].m;
let sortOrder = this._sortOrder;
let pp = ship.getPowerPlant();
return (
<div ref={node => this.node = node} className='group half' id='componentPriority'>
<table style={{ width: '100%' }}>
<thead>
<tr className='main'>
<th colSpan='2' className='sortable le' onClick={sortOrder.bind(this, 'n')} >{translate('module')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 't')} >{translate('type')}</th>
<th style={{ width: '4em' }} className='sortable' onClick={sortOrder.bind(this, 'pri')} >{translate('pri')}</th>
<th colSpan='2' className='sortable' onClick={sortOrder.bind(this, 'pwr')} >{translate('PWR')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'r')} >{translate('ret')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={sortOrder.bind(this, 'd')} >{translate('dep')}</th>
<th colSpan='2' className='sortable le' onClick={() => this._sortOrder('n')} >{translate('module')}</th>
{/* <th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('t')} >{translate('type')}</th> */}
<th style={{ width: '4em' }} className='sortable' onClick={() => this._sortOrder('pri')} >{translate('pri')}</th>
<th colSpan='2' className='sortable' onClick={() => this._sortOrder('pwr')} >{translate('PWR')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('r')} >{translate('ret')}</th>
<th style={{ width: '3em' }} className='sortable' onClick={() => this._sortOrder('d')} >{translate('dep')}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{pp.class + pp.rating}</td>
<td>{String(pp.getClass()) + pp.getRating()}</td>
<td className='le shorten cap' >{translate('pp')}</td>
<td><u >{translate('SYS')}</u></td>
<td>1</td>
<td className='ri'>{pwr(pp.getPowerGeneration())}</td>
<td className='ri'>{formats.f2(pp.get('powercapacity'))}</td>
<td className='ri'><u>100%</u></td>
<td></td>
<td></td>
</tr>
<tr><td style={{ lineHeight:0 }} colSpan='8'><hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} /></td></tr>
{this._renderPowerRows(ship, translate, pwr, formats.pct1)}
<tr><td style={{ lineHeight:0 }} colSpan='8'>
<hr style={{ margin: '0 0 3px', background: '#ff8c0d', border: 0, height: 1 }} />
</td></tr>
{this._renderPowerRows(ship, translate, formats.f2, formats.pct1)}
</tbody>
</table>
<PowerBands width={this.state.width} code={code} available={pp.getPowerGeneration()} bands={ship.priorityBands} />
<PowerBands width={this.state.width} ship={ship} code={code} />
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { Ships } from 'coriolis-data/dist';
import { Rocket } from './SvgIcons';
import Persist from '../stores/Persist';
import cn from 'classnames';
import autoBind from 'auto-bind';
/**
* Ship picker
@@ -26,13 +27,9 @@ export default class ShipPicker extends TranslatedComponent {
* @param {object} props Properties react
* @param {object} context react context
*/
constructor(props, context) { // eslint-disable-line
constructor(props, context) {
super(props);
this.shipOrder = Object.keys(Ships).sort();
this._toggleMenu = this._toggleMenu.bind(this);
this._closeMenu = this._closeMenu.bind(this);
autoBind(this);
this.state = { menuOpen: false };
}

View File

@@ -1,21 +1,24 @@
import autoBind from 'auto-bind';
import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames';
import { Warning } from './SvgIcons';
import * as Calc from '../shipyard/Calculations';
import { ShipProps } from 'ed-forge';
const {
SPEED, BOOST_SPEED, DAMAGE_METRICS, JUMP_METRICS, SHIELD_METRICS,
ARMOUR_METRICS, CARGO_CAPACITY, FUEL_CAPACITY, UNLADEN_MASS, MAXIMUM_MASS,
MODULE_PROTECTION_METRICS, PASSENGER_CAPACITY
} = ShipProps;
/**
* Ship Summary Table / Stats
*/
export default class ShipSummaryTable extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
cargo: PropTypes.number.isRequired,
fuel: PropTypes.number.isRequired,
marker: PropTypes.string.isRequired,
pips: PropTypes.object.isRequired
code: PropTypes.string.isRequired,
};
/**
@@ -24,7 +27,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
*/
constructor(props) {
super(props);
this.didContextChange = this.didContextChange.bind(this);
autoBind(this);
this.state = {
shieldColour: 'blue'
};
@@ -35,44 +38,54 @@ export default class ShipSummaryTable extends TranslatedComponent {
* @return {React.Component} Summary table
*/
render() {
const { ship, cargo, fuel, pips } = this.props;
const { ship } = this.props;
let { language, tooltip, termtip } = this.context;
let translate = language.translate;
let u = language.units;
let formats = language.formats;
let { time, int, round, f1, f2 } = formats;
let { time, int, f1, f2 } = formats;
let hide = tooltip.bind(null, null);
const shieldGenerator = ship.findInternalByGroup('sg') || ship.findInternalByGroup('psg');
const sgClassNames = cn({ warning: shieldGenerator && !ship.shield, muted: !shieldGenerator });
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
const timeToDrain = Calc.timeToDrainWep(ship, 4);
const canThrust = ship.canThrust(cargo, ship.fuelCapacity);
const speed = ship.get(SPEED);
const shipBoost = ship.get(BOOST_SPEED);
const canThrust = 0 < speed;
const canBoost = canThrust && !isNaN(shipBoost);
const speedTooltip = canThrust ? 'TT_SUMMARY_SPEED' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
const canBoost = ship.canBoost(cargo, ship.fuelCapacity);
const boostTooltip = canBoost ? 'TT_SUMMARY_BOOST' : canThrust ? 'TT_SUMMARY_BOOST_NONFUNCTIONAL' : 'TT_SUMMARY_SPEED_NONFUNCTIONAL';
const canJump = ship.getSlotStatus(ship.standard[2]) == 3;
const sgMetrics = Calc.shieldMetrics(ship, pips.sys);
const shipBoost = canBoost ? Calc.calcBoost(ship) : 'No Boost';
const restingHeat = Math.sqrt(((ship.standard[0].m.pgen * ship.standard[0].m.eff) / ship.heatCapacity) / 0.2);
const armourMetrics = Calc.armourMetrics(ship);
const sgMetrics = ship.get(SHIELD_METRICS);
const armourMetrics = ship.get(ARMOUR_METRICS);
const damageMetrics = ship.get(DAMAGE_METRICS);
const moduleProtectionMetrics = ship.get(MODULE_PROTECTION_METRICS);
const timeToDrain = damageMetrics.timeToDrain[8];
const shieldGenerator = ship.getShieldGenerator();
const sgClassNames = cn({
warning: shieldGenerator && !shieldGenerator.isEnabled(),
muted: !shieldGenerator,
});
const sgTooltip = shieldGenerator ? 'TT_SUMMARY_SHIELDS' : 'TT_SUMMARY_SHIELDS_NONFUNCTIONAL';
const sgType = shieldGenerator ? shieldGenerator.readMeta('type') : undefined;
let shieldColour = 'blue';
if (shieldGenerator && shieldGenerator.m.grp === 'psg') {
shieldColour = 'green';
} else if (shieldGenerator && shieldGenerator.m.grp === 'bsg') {
shieldColour = 'purple';
switch (sgType) {
case 'biweaveshieldgen': shieldColour = 'purple'; break;
case 'prismaticshieldgen': shieldColour = 'green'; break;
}
this.state = {
shieldColour
};
this.state = { shieldColour };
const jumpRangeMetrics = ship.getMetrics(JUMP_METRICS);
// TODO:
const canJump = true;
return <div id='summary'>
<div style={{display: "table", width: "100%"}}>
<div style={{display: "table-row"}}>
<div style={{ display: 'table', width: '100%' }}>
<div style={{ display: 'table-row' }}>
<table className={'summaryTable'}>
<thead>
<tr className='main'>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canThrust }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': speed == 0 }) }>{translate('speed')}</th>
<th rowSpan={2} className={ cn({ 'bg-warning-disabled': !canBoost }) }>{translate('boost')}</th>
<th colSpan={5} className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('jump range')}</th>
<th colSpan={5} className={ cn({ 'bg-warning-disabled': jumpRangeMetrics.jumpRange == 0 }) }>{translate('jump range')}</th>
<th rowSpan={2}>{translate('shield')}</th>
<th rowSpan={2}>{translate('integrity')}</th>
<th rowSpan={2}>{translate('DPS')}</th>
@@ -90,11 +103,11 @@ export default class ShipSummaryTable extends TranslatedComponent {
<th rowSpan={2}>{translate('resting heat (Beta)')}</th>
</tr>
<tr>
<th className={ cn({ 'lft': true, 'bg-warning-disabled': !canJump }) }>{translate('max')}</th>
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('unladen')}</th>
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('laden')}</th>
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('total unladen')}</th>
<th className={ cn({ 'bg-warning-disabled': !canJump }) }>{translate('total laden')}</th>
<th className="lft">{translate('max')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
<th>{translate('total unladen')}</th>
<th>{translate('total laden')}</th>
<th className='lft'>{translate('hull')}</th>
<th>{translate('unladen')}</th>
<th>{translate('laden')}</th>
@@ -102,30 +115,87 @@ export default class ShipSummaryTable extends TranslatedComponent {
</thead>
<tbody>
<tr>
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })} onMouseLeave={hide}>{ canThrust ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, false))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })} onMouseLeave={hide}>{ canBoost ? <span>{int(ship.calcSpeed(4, ship.fuelCapacity, 0, true))}{u['m/s']}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{ f2(Calc.jumpRange(ship.unladenMass + ship.standard[2].m.getMaxFuelPerJump(), ship.standard[2].m, ship.standard[2].m.getMaxFuelPerJump(), ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.jumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })} onMouseLeave={hide}>{ canJump ? <span>{f2(Calc.totalJumpRange(ship.unladenMass + ship.fuelCapacity + ship.cargoCapacity, ship.standard[2].m, ship.fuelCapacity, ship))}{u.LY}</span> : <span className='warning'>0 <Warning/></span> }</td>
<td className={sgClassNames} onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })} onMouseLeave={hide}>{int(ship.shield)}{u.MJ}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })} onMouseLeave={hide}>{int(ship.armour)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalDps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })} onMouseLeave={hide}>{f1(ship.totalEps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })} onMouseLeave={hide}>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
<td onMouseEnter={termtip.bind(null, speedTooltip, { cap: 0 })}
onMouseLeave={hide}
>{canThrust ?
<span>{int(speed)}{u['m/s']}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, boostTooltip, { cap: 0 })}
onMouseLeave={hide}
>{canBoost ?
<span>{int(shipBoost)}{u['m/s']}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_MAX_SINGLE_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
// TODO:
<span>{NaN}{u.LY}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_SINGLE_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
// TODO:
<span>{NaN}{u.LY}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_SINGLE_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
<span>{f2(jumpRangeMetrics.jumpRange)}{u.LY}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_TOTAL_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
// TODO:
<span>{NaN}{u.LY}</span> :
<span className='warning'>0 <Warning/></span>
}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_TOTAL_JUMP', { cap: 0 })}
onMouseLeave={hide}
>{canJump ?
<span>{f2(jumpRangeMetrics.totalRange)}{u.LY}</span> :
<span className='warning'>0<Warning/></span>
}</td>
<td className={sgClassNames}
onMouseEnter={termtip.bind(null, sgTooltip, { cap: 0 })}
onMouseLeave={hide}
>{int(sgMetrics.shieldStrength)}{u.MJ}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_INTEGRITY', { cap: 0 })}
onMouseLeave={hide}
>{int(armourMetrics.armour)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_DPS', { cap: 0 })}
onMouseLeave={hide}
>{f1(damageMetrics.dps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_EPS', { cap: 0 })}
onMouseLeave={hide}
>{f1(damageMetrics.eps)}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_TTD', { cap: 0 })}
onMouseLeave={hide}
>{timeToDrain === Infinity ? '∞' : time(timeToDrain)}</td>
{/* <td>{f1(ship.totalHps)}</td> */}
<td>{round(ship.cargoCapacity)}{u.T}</td>
<td>{ship.passengerCapacity}</td>
<td>{round(ship.fuelCapacity)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })} onMouseLeave={hide}>{ship.hullMass}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.unladenMass)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })} onMouseLeave={hide}>{int(ship.ladenMass)}{u.T}</td>
<td>{int(ship.hardness)}</td>
<td>{ship.crew}</td>
<td>{ship.masslock}</td>
<td>{shipBoost !== 'No Boost' ? formats.time(shipBoost) : 'No Boost'}</td>
<td>{formats.pct(restingHeat)}</td>
<td>{ship.get(CARGO_CAPACITY)}{u.T}</td>
<td>{ship.get(PASSENGER_CAPACITY)}</td>
<td>{ship.get(FUEL_CAPACITY)}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_HULL_MASS', { cap: 0 })}
onMouseLeave={hide}
>{ship.readProp('hullmass')}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_UNLADEN_MASS', { cap: 0 })}
onMouseLeave={hide}
>{int(ship.get(UNLADEN_MASS))}{u.T}</td>
<td onMouseEnter={termtip.bind(null, 'TT_SUMMARY_LADEN_MASS', { cap: 0 })}
onMouseLeave={hide}
>{int(ship.get(MAXIMUM_MASS))}{u.T}</td>
<td>{int(ship.readProp('hardness'))}</td>
<td>{ship.readMeta('crew')}</td>
<td>{ship.readProp('masslock')}</td>
{/* TODO: boost intervall */}
<td>{NaN}</td>
{/* TODO: resting heat */}
<td>{NaN}</td>
</tr>
</tbody>
</table>
@@ -154,19 +224,19 @@ export default class ShipSummaryTable extends TranslatedComponent {
</thead>
<tbody>
<tr>
<td>{translate(shieldGenerator && shieldGenerator.m.grp || 'No Shield')}</td>
<td>{formats.pct1(ship.shieldExplRes)}</td>
<td>{formats.pct1(ship.shieldKinRes)}</td>
<td>{formats.pct1(ship.shieldThermRes)}</td>
<td>{translate(sgType || 'No Shield')}</td>
<td>{formats.pct1(1 - sgMetrics.explosive.damageMultiplier)}</td>
<td>{formats.pct1(1 - sgMetrics.kinetic.damageMultiplier)}</td>
<td>{formats.pct1(1 - sgMetrics.thermal.damageMultiplier)}</td>
<td></td>
<td>{int(ship && sgMetrics.summary > 0 ? sgMetrics.summary : 0)}{u.MJ}</td>
<td>{int(ship && sgMetrics.summary > 0 ? sgMetrics.summary / sgMetrics.explosive.base : 0)}{u.MJ}</td>
<td>{int(ship && sgMetrics.summary ? sgMetrics.summary / sgMetrics.kinetic.base : 0)}{u.MJ}</td>
<td>{int(ship && sgMetrics.summary ? sgMetrics.summary / sgMetrics.thermal.base : 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.explosive.damageMultiplier || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.kinetic.damageMultiplier || 0)}{u.MJ}</td>
<td>{int(sgMetrics.shieldStrength / sgMetrics.thermal.damageMultiplier || 0)}{u.MJ}</td>
<td></td>
<td>{sgMetrics && sgMetrics.recover === Math.Inf ? translate('Never') : formats.time(sgMetrics.recover)}</td>
<td>{sgMetrics && sgMetrics.recharge === Math.Inf ? translate('Never') : formats.time(sgMetrics.recharge)}</td>
<td>{formats.time(sgMetrics.recover) || translate('Never')}</td>
<td>{formats.time(sgMetrics.recharge) || translate('Never')}</td>
</tr>
</tbody>
<thead>
@@ -193,19 +263,18 @@ export default class ShipSummaryTable extends TranslatedComponent {
</thead>
<tbody>
<tr>
<td>{translate(ship && ship.bulkheads && ship.bulkheads.m && ship.bulkheads.m.name || 'No Armour')}</td>
<td>{formats.pct1(ship.hullExplRes)}</td>
<td>{formats.pct1(ship.hullKinRes)}</td>
<td>{formats.pct1(ship.hullThermRes)}</td>
<td>{formats.pct1(ship.hullCausRes)}</td>
<td>{int(armourMetrics.total)}</td>
<td>{int(armourMetrics.total / armourMetrics.explosive.total)}</td>
<td>{int(armourMetrics.total/ armourMetrics.kinetic.total)}</td>
<td>{int(armourMetrics.total / armourMetrics.thermal.total)}</td>
<td>{int(armourMetrics.total/ armourMetrics.caustic.total)}</td>
<td>{int(armourMetrics.modulearmour)}</td>
<td>{int(armourMetrics.moduleprotection * 100) + '%'}</td>
<td>{translate(ship.getAlloys().readMeta('type') || 'No Armour')}</td>
<td>{formats.pct1(1 - armourMetrics.explosive.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.kinetic.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.thermal.damageMultiplier)}</td>
<td>{formats.pct1(1 - armourMetrics.caustic.damageMultiplier)}</td>
<td>{int(armourMetrics.armour)}</td>
<td>{int(armourMetrics.armour / armourMetrics.explosive.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.kinetic.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.thermal.damageMultiplier)}</td>
<td>{int(armourMetrics.armour / armourMetrics.caustic.damageMultiplier)}</td>
<td>{int(moduleProtectionMetrics.moduleArmour)}</td>
<td>{formats.pct1(1 - moduleProtectionMetrics.moduleProtection)}</td>
</tr>
</tbody>
</table>

View File

@@ -2,30 +2,38 @@ import React from 'react';
import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import cn from 'classnames';
import { ListModifications, Modified } from './SvgIcons';
import AvailableModulesMenu from './AvailableModulesMenu';
import ModificationsMenu from './ModificationsMenu';
import { diffDetails } from '../utils/SlotFunctions';
import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { stopCtxPropagation, wrapCtxMenu } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
import { Module } from 'ed-forge';
import { REG_MILITARY_SLOT, REG_HARDPOINT_SLOT } from 'ed-forge/lib/data/slots';
import autoBind from 'auto-bind';
import { toPairs } from 'lodash';
const HARDPOINT_SLOT_LABELS = {
1: 'S',
2: 'M',
3: 'L',
4: 'H',
};
/**
* Abstract Slot
*/
export default class Slot extends TranslatedComponent {
static propTypes = {
availableModules: PropTypes.func.isRequired,
onSelect: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired,
maxClass: PropTypes.number.isRequired,
selected: PropTypes.bool,
m: PropTypes.object,
enabled: PropTypes.bool.isRequired,
ship: PropTypes.object.isRequired,
eligible: PropTypes.object,
currentMenu: PropTypes.any,
hideSearch: PropTypes.bool,
m: PropTypes.instanceOf(Module),
warning: PropTypes.func,
drag: PropTypes.func,
drop: PropTypes.func,
dropClass: PropTypes.string
dropClass: PropTypes.string,
propsToShow: PropTypes.object.isRequired,
onPropToggle: PropTypes.func.isRequired,
};
/**
@@ -34,25 +42,122 @@ export default class Slot extends TranslatedComponent {
*/
constructor(props) {
super(props);
this._modificationsSelected = false;
this._contextMenu = wrapCtxMenu(this._contextMenu.bind(this));
this._getMaxClassLabel = this._getMaxClassLabel.bind(this);
this._keyDown = this._keyDown.bind(this);
this.slotDiv = null;
autoBind(this);
this.state = { menuIndex: 0 };
}
// Must be implemented by subclasses:
// _getSlotDetails()
/**
* Opens a menu while setting state.
* @param {Object} newMenuIndex New menu index
* @param {Event} event Event object
*/
_openMenu(newMenuIndex, event) {
const slotName = this.props.m.getSlot();
if (
this.props.currentMenu === slotName &&
newMenuIndex === this.state.menuIndex
) {
this.context.closeMenu();
} {
this.setState({ menuIndex: newMenuIndex });
this.context.openMenu(slotName);
}
// If we don't stop event propagation, the underlying divs also might
// get clicked which would open up other menus
event.stopPropagation();
}
/**
* Get the CSS class name for the slot. Can/should be overriden
* as necessary.
* @return {string} CSS Class name
* Generate the slot contents
* @return {React.Component} Slot contents
*/
_getClassNames() {
_getSlotDetails() {
const { m, propsToShow } = this.props;
let { termtip, tooltip, language } = this.context;
const { translate, units, formats } = language;
if (m.isEmpty()) {
return <div className="empty">
{translate(
m.getSlot().match(REG_MILITARY_SLOT) ? 'emptyrestricted' : 'empty'
)}
</div>;
} else {
let classRating = m.getClassRating();
let { drag, drop } = this.props;
// Modifications tooltip shows blueprint and grade, if available
let modTT = translate('modified');
const blueprint = m.getBlueprint();
const experimental = m.getExperimental();
const grade = m.getBlueprintGrade();
if (blueprint) {
modTT = `${translate(blueprint)} ${translate('grade')}: ${grade}`;
if (experimental) {
modTT += `, ${translate(experimental)}`;
}
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(language, m)}
</div>
);
}
let mass = m.get('mass') || m.get('cargo') || m.get('fuel') || 0;
return (
<div
className={cn('details', { disabled: !m.isEnabled() })}
draggable="true"
onDragStart={drag}
onDragEnd={drop}
>
<div className={'cb'}>
<div className={'l'}>
{classRating} {translate(m.readMeta('type'))}
{blueprint && (
<span
onMouseOver={termtip.bind(null, modTT)}
onMouseOut={tooltip.bind(null, null)}
>
<Modified />
</span>
)}
</div>
{propsToShow.mass ?
<div className={'r'}>
{formats.round(mass)}
{units.T}
</div> : null}
</div>
<div className={'cb'}>
{toPairs(propsToShow).sort().map(([prop, show]) => {
const { unit, value } = m.getFormatted(prop, true);
// Don't show mass again; it's already shown on the top right
// corner of a slot
if (!show || isNaN(value) || prop == 'mass') {
return null;
} else {
return (<div className='l'>
{translate(prop)}: {formats.round(value)}{unit}
</div>);
}
})}
{(m.getApplicableBlueprints() || []).length > 0 ? (
<div className="r">
<button onClick={this._openMenu.bind(this, 1)}
onContextMenu={stopCtxPropagation}
onMouseOver={termtip.bind(null, translate('modifications'))}
onMouseOut={tooltip.bind(null, null)}
>
<ListModifications />
</button>
</div>
) : null}
</div>
</div>
);
}
}
/**
@@ -61,7 +166,19 @@ export default class Slot extends TranslatedComponent {
* @return {string} label
*/
_getMaxClassLabel() {
return this.props.maxClass;
const { m } = this.props;
let size = m.getSize();
switch (true) {
case m.getSlot() === 'armour':
return '';
case size === 0:
// This can also happen for armour but that case was handled above
return 'U';
case Boolean(m.getSlot().match(REG_HARDPOINT_SLOT)):
return HARDPOINT_SLOT_LABELS[size];
default:
return size;
}
}
/**
@@ -71,25 +188,15 @@ export default class Slot extends TranslatedComponent {
_contextMenu(event) {
event.stopPropagation();
event.preventDefault();
this.props.onSelect(null,null);
const { m } = this.props;
m.reset();
if (this.props.currentMenu === m.getSlot()) {
this.context.closeMenu();
} else {
this.forceUpdate();
}
}
/** Key Down handler
* @param {SyntheticEvent} event Event
* ToDo: see if this can be moved up
* we do more or less the same thing
* in every section when Enter key is pressed
* on a focusable item
*
*/
_keyDown(event) {
if (event.key == 'Enter') {
if(event.target.className == 'r') {
this._toggleModifications();
}
this.props.onOpen(event);
}
}
/**
* Render the slot
* @return {React.Component} The slot
@@ -97,64 +204,42 @@ export default class Slot extends TranslatedComponent {
render() {
let language = this.context.language;
let translate = language.translate;
let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props;
let slotDetails, modificationsMarker, menu;
if (!selected) {
// If not selected then sure that modifications flag is unset
this._modificationsSelected = false;
}
if (m) {
slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes
modificationsMarker = JSON.stringify(m);
} else {
slotDetails = <div className={'empty'}>{translate(eligible ? 'emptyrestricted' : 'empty')}</div>;
modificationsMarker = '';
}
if (selected) {
if (this._modificationsSelected) {
menu = <ModificationsMenu
className={this._getClassNames()}
onChange={onChange}
ship={ship}
m={m}
marker={modificationsMarker}
modButton = {this.modButton}
/>;
} else {
menu = <AvailableModulesMenu
className={this._getClassNames()}
modules={availableModules()}
m={m}
ship={ship}
onSelect={onSelect}
warning={warning}
diffDetails={diffDetails.bind(ship, this.context.language)}
slotDiv = {this.slotDiv}
/>;
}
}
let {
currentMenu, m, dropClass, dragOver, warning, hideSearch, propsToShow,
onPropToggle,
} = this.props;
const { menuIndex } = this.state;
// TODO: implement touch dragging
const selected = currentMenu === m.getSlot();
return (
<div className={cn('slot', dropClass, { selected })} onClick={onOpen} onKeyDown={this._keyDown} onContextMenu={this._contextMenu} onDragOver={dragOver} tabIndex="0" ref={slotDiv => this.slotDiv = slotDiv}>
<div className='details-container'>
<div className='sz'>{this._getMaxClassLabel(translate)}</div>
{slotDetails}
<div
className={cn('slot', dropClass, { selected })}
onContextMenu={this._contextMenu}
onDragOver={dragOver} tabIndex="0"
onClick={this._openMenu.bind(this, 0)}
>
<div className={cn(
'details-container',
{ warning: warning && warning(m) },
)}>
<div className="sz">{this._getMaxClassLabel(translate)}</div>
{this._getSlotDetails()}
</div>
{menu}
{selected && menuIndex === 0 &&
<AvailableModulesMenu
m={m} hideSearch={hideSearch}
onSelect={(item) => {
m.setItem(item);
this.context.closeMenu();
}}
warning={warning}
// diffDetails={diffDetails.bind(ship, this.context.language)}
/>}
{selected && menuIndex === 1 &&
<ModificationsMenu m={m} propsToShow={propsToShow}
onPropToggle={onPropToggle} />}
</div>
);
}
/**
* Toggle the modifications flag when selecting the modifications icon
*/
_toggleModifications() {
this._modificationsSelected = !this._modificationsSelected;
}
}

View File

@@ -5,47 +5,33 @@ import { wrapCtxMenu } from '../utils/UtilityFunctions';
import { canMount } from '../utils/SlotFunctions';
import { Equalizer } from '../components/SvgIcons';
import cn from 'classnames';
import { Ship } from 'ed-forge';
import autoBind from 'auto-bind';
const browser = require('detect-browser');
/**
* Abstract Slot Section
*/
export default class SlotSection extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onCargoChange: PropTypes.func.isRequired,
onFuelChange: PropTypes.func.isRequired,
ship: PropTypes.instanceOf(Ship),
code: PropTypes.string.isRequired,
togglePwr: PropTypes.func,
sectionMenuRefs: PropTypes.object
propsToShow: PropTypes.object.isRequired,
onPropToggle: PropTypes.func.isRequired,
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
* @param {string} sectionId Section DOM Id
* @param {string} sectionName Section name
*/
constructor(props, context, sectionId, sectionName) {
constructor(props, sectionName) {
super(props);
this.sectionId = sectionId;
this.sectionName = sectionName;
this.ssHeadRef = null;
autoBind(this);
this.sectionName = sectionName;
this.sectionRefArr = this.props.sectionMenuRefs[this.sectionId] = [];
this.sectionRefArr['selectedRef'] = null;
this._getSlots = this._getSlots.bind(this);
this._selectModule = this._selectModule.bind(this);
this._getSectionMenu = this._getSectionMenu.bind(this);
this._contextMenu = this._contextMenu.bind(this);
this._drop = this._drop.bind(this);
this._dragOverNone = this._dragOverNone.bind(this);
this._close = this._close.bind(this);
this._keyDown = this._keyDown.bind(this);
this._handleSectionFocus = this._handleSectionFocus.bind(this);
this.state = {};
}
@@ -55,82 +41,6 @@ export default class SlotSection extends TranslatedComponent {
// _contextMenu()
// componentDidUpdate(prevProps)
/**
* TODO: May either need to send the function to be triggered when Enter key is pressed, or else
* may need a separate keyDown handler for each subclass (StandardSlotSection, HardpointSlotSection, etc.)
* ex: _keyDown(_keyDownfn, event)
*
* @param {SyntheticEvent} event KeyDown event
*/
_keyDown(event) {
if (event.key == 'Enter') {
event.stopPropagation();
if (event.currentTarget.nodeName === 'H1') {
this._openMenu(this.sectionName, event);
} else {
event.currentTarget.click();
}
return;
}
if (event.key == 'Tab') {
if (event.shiftKey) {
if ((event.currentTarget === this.sectionRefArr[this.firstRefId]) && this.sectionRefArr[this.lastRefId]) {
event.preventDefault();
this.sectionRefArr[this.lastRefId].focus();
}
} else {
if ((event.currentTarget === this.sectionRefArr[this.lastRefId]) && this.sectionRefArr[this.firstRefId]) {
event.preventDefault();
this.sectionRefArr[this.firstRefId].focus();
}
}
}
}
/**
* Set focus on appropriate Slot Section Menu element
* @param {Object} focusPrevProps prevProps for componentDidUpdate() from ...SlotSection.jsx
* @param {String} firstRef id of the first ref in ...SlotSection.jsx
* @param {String} lastRef id of the last ref in ...SlotSection.jsx
*
*/
_handleSectionFocus(focusPrevProps, firstRef, lastRef) {
if (this.selectedRefId !== null && this.sectionRefArr[this.selectedRefId]) {
// set focus on the previously selected option for the currently open section menu
this.sectionRefArr[this.selectedRefId].focus();
} else if (this.sectionRefArr[firstRef] && this.sectionRefArr[firstRef] != null) {
// set focus on the first option in the currently open section menu if none have been selected previously
this.sectionRefArr[firstRef].focus();
} else if (this.props.currentMenu == null && focusPrevProps.currentMenu == this.sectionName && this.sectionRefArr['ssHeadRef']) {
// set focus on the section menu header when section menu is closed
this.sectionRefArr['ssHeadRef'].focus();
}
}
/**
* Open a menu
* @param {string} menu Menu name
* @param {SyntheticEvent} event Event
*/
_openMenu(menu, event) {
event.preventDefault();
event.stopPropagation();
if (this.props.currentMenu === menu) {
menu = null;
}
this.context.openMenu(menu);
}
/**
* Mount/Use the specified module in the slot
* @param {Object} slot Slot
* @param {Object} m Selected module
*/
_selectModule(slot, m) {
this.props.ship.use(slot, m, false);
this.props.onChange();
this._close();
}
/**
* Slot Drag Handler
* @param {object} originSlot Origin slot model
@@ -207,7 +117,6 @@ export default class SlotSection extends TranslatedComponent {
// Copy power info
targetSlot.enabled = originSlot.enabled;
targetSlot.priority = originSlot.priority;
this.props.onChange();
}
} else {
// Store power info
@@ -236,7 +145,6 @@ export default class SlotSection extends TranslatedComponent {
targetSlot.enabled = targetEnabled;
targetSlot.priority = targetPriority;
}
this.props.onChange();
this.props.ship
.updatePowerGenerated()
.updatePowerUsed()
@@ -282,6 +190,17 @@ export default class SlotSection extends TranslatedComponent {
return 'ineligible'; // Cannot be dropped / invalid drop slot
}
_open(newMenu, event) {
event.preventDefault();
event.stopPropagation();
const { currentMenu } = this.props;
if (currentMenu === newMenu) {
this.context.closeMenu();
} else {
this.context.openMenu(newMenu);
}
}
/**
* Close current menu
*/
@@ -298,14 +217,13 @@ export default class SlotSection extends TranslatedComponent {
render() {
let translate = this.context.language.translate;
let sectionMenuOpened = this.props.currentMenu === this.sectionName;
let open = this._openMenu.bind(this, this.sectionName);
let ctx = wrapCtxMenu(this._contextMenu);
return (
<div id={this.sectionId} className={'group'} onDragLeave={this._dragOverNone}>
<div className={cn('section-menu', { selected: sectionMenuOpened })} onClick={open} onContextMenu={ctx}>
<h1 tabIndex="0" onKeyDown={this._keyDown} ref={ssHead => this.sectionRefArr['ssHeadRef'] = ssHead}>{translate(this.sectionName)} <Equalizer/></h1>
{sectionMenuOpened ? this._getSectionMenu(translate, this.props.ship) : null }
<div className="group" onDragLeave={this._dragOverNone}>
<div className={cn('section-menu', { selected: sectionMenuOpened })}
onContextMenu={wrapCtxMenu(this._contextMenu)} onClick={this._open.bind(this, this.sectionName)}>
<h1 tabIndex="0">{translate(this.sectionName)}<Equalizer/></h1>
{sectionMenuOpened && this._getSectionMenu()}
</div>
{this._getSlots()}
</div>

View File

@@ -1,162 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import cn from 'classnames';
import Persist from '../stores/Persist';
import TranslatedComponent from './TranslatedComponent';
import { diffDetails } from '../utils/SlotFunctions';
import AvailableModulesMenu from './AvailableModulesMenu';
import ModificationsMenu from './ModificationsMenu';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { ListModifications, Modified } from './SvgIcons';
import { Modifications } from 'coriolis-data/dist';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import { blueprintTooltip } from '../utils/BlueprintFunctions';
/**
* Standard Slot
*/
export default class StandardSlot extends TranslatedComponent {
static propTypes = {
slot: PropTypes.object,
modules: PropTypes.array.isRequired,
onSelect: PropTypes.func.isRequired,
onOpen: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
ship: PropTypes.object.isRequired,
selected: PropTypes.bool,
warning: PropTypes.func,
};
/**
* Construct the slot
* @param {object} props Object properties
*/
constructor(props) {
super(props);
this._modificationsSelected = false;
this._keyDown = this._keyDown.bind(this);
this.modButton = null;
this.slotDiv = null;
}
/**
* Handle Enter key
* @param {SyntheticEvent} event KeyDown event
*/
_keyDown(event) {
if (event.key == 'Enter') {
if(event.target.className == 'r') {
this._toggleModifications();
}
this.props.onOpen(event);
}
}
/**
* Render the slot
* @return {React.Component} Slot component
*/
render() {
let { termtip, tooltip } = this.context;
let { translate, formats, units } = this.context.language;
let { modules, slot, selected, warning, onSelect, onChange, ship } = this.props;
let m = slot.m;
let classRating = m.class + m.rating;
let menu;
let validMods = m == null || !Modifications.modules[m.grp] ? [] : (Modifications.modules[m.grp].modifications || []);
if (m && m.name && m.name === 'Guardian Hybrid Power Plant') {
validMods = [];
}
if (m && m.name && m.name === 'Guardian Power Distributor') {
validMods = [];
}
let showModuleResistances = Persist.showModuleResistances();
let mass = m.getMass() || m.cargo || m.fuel || 0;
// Modifications tooltip shows blueprint and grade, if available
let modTT = translate('modified');
if (m && m.blueprint && m.blueprint.name) {
modTT = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id >= 0) {
modTT += ', ' + translate(m.blueprint.special.name);
}
modTT = (
<div>
<div>{modTT}</div>
{blueprintTooltip(translate, m.blueprint.grades[m.blueprint.grade], null, m.grp, m)}
</div>
);
}
if (!selected) {
// If not selected then sure that modifications flag is unset
this._modificationsSelected = false;
}
const modificationsMarker = JSON.stringify(m);
if (selected) {
if (this._modificationsSelected) {
menu = <ModificationsMenu
className='standard'
onChange={onChange}
ship={ship}
m={m}
marker={modificationsMarker}
modButton = {this.modButton}
/>;
} else {
menu = <AvailableModulesMenu
className='standard'
modules={modules}
m={m}
ship={ship}
onSelect={onSelect}
warning={warning}
diffDetails={diffDetails.bind(ship, this.context.language)}
slotDiv = {this.slotDiv}
/>;
}
}
return (
<div className={cn('slot', { selected: this.props.selected })} onClick={this.props.onOpen} onKeyDown={this._keyDown} onContextMenu={stopCtxPropagation} tabIndex="0" ref={ slotDiv => this.slotDiv = slotDiv }>
<div className={cn('details-container', { warning: warning && warning(slot.m), disabled: m.grp !== 'bh' && !slot.enabled })}>
<div className={'sz'}>{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}</div>
<div>
<div className={'l'}>{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? <span className='r' onMouseOver={termtip.bind(null, modTT)} onMouseOut={tooltip.bind(null, null)}><Modified /></span> : null }</div>
<div className={'r'}>{formats.round(mass)}{units.T}</div>
<div/>
<div className={'cb'}>
{ m.getMinMass() ? <div className='l'>{translate('minimum mass')}: {formats.int(m.getMinMass())}{units.T}</div> : null }
{ m.getOptMass() ? <div className='l'>{translate('optimal mass')}: {formats.int(m.getOptMass())}{units.T}</div> : null }
{ m.getMaxMass() ? <div className='l'>{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}</div> : null }
{ m.getOptMul() ? <div className='l'>{translate('optimal multiplier')}: {formats.rPct(m.getOptMul())}</div> : null }
{ m.getRange() ? <div className='l'>{translate('range', m.grp)}: {formats.f2(m.getRange())}{units.km}</div> : null }
{ m.time ? <div className='l'>{translate('time')}: {formats.time(m.time)}</div> : null }
{ m.getThermalEfficiency() ? <div className='l'>{translate('efficiency')}: {formats.f2(m.getThermalEfficiency())}</div> : null }
{ m.getPowerGeneration() > 0 ? <div className='l'>{translate('pgen')}: {formats.f1(m.getPowerGeneration())}{units.MW}</div> : null }
{ m.getMaxFuelPerJump() ? <div className='l'>{translate('max')} {translate('fuel')}: {formats.f1(m.getMaxFuelPerJump())}{units.T}</div> : null }
{ m.getWeaponsCapacity() ? <div className='l'>{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}</div> : null }
{ m.getSystemsCapacity() ? <div className='l'>{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}</div> : null }
{ m.getEnginesCapacity() ? <div className='l'>{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}</div> : null }
{ showModuleResistances && m.getExplosiveResistance() ? <div className='l'>{translate('explres')}: {formats.pct(m.getExplosiveResistance())}</div> : null }
{ showModuleResistances && m.getKineticResistance() ? <div className='l'>{translate('kinres')}: {formats.pct(m.getKineticResistance())}</div> : null }
{ showModuleResistances && m.getThermalResistance() ? <div className='l'>{translate('thermres')}: {formats.pct(m.getThermalResistance())}</div> : null }
{ m.getIntegrity() ? <div className='l'>{translate('integrity')}: {formats.int(m.getIntegrity())}</div> : null }
{ validMods.length > 0 ? <div className='r' tabIndex="0" ref={ modButton => this.modButton = modButton }><button tabIndex="-1" onClick={this._toggleModifications.bind(this)} onContextMenu={stopCtxPropagation} onMouseOver={termtip.bind(null, 'modifications')} onMouseOut={tooltip.bind(null, null)}><ListModifications /></button></div> : null }
</div>
</div>
</div>
{menu}
</div>
);
}
/**
* Toggle the modifications flag when selecting the modifications icon
*/
_toggleModifications() {
this._modificationsSelected = !this._modificationsSelected;
}
}

View File

@@ -1,46 +1,33 @@
import React from 'react';
import cn from 'classnames';
import SlotSection from './SlotSection';
import StandardSlot from './StandardSlot';
import Slot from './Slot';
import Module from '../shipyard/Module';
import * as ShipRoles from '../shipyard/ShipRoles';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import autoBind from 'auto-bind';
import { stopCtxPropagation, moduleGet } from '../utils/UtilityFunctions';
import { ShipProps } from 'ed-forge';
const { CONSUMED_RETR, LADEN_MASS } = ShipProps;
/**
* Standard Slot section
*/
export default class StandardSlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context, 'standard', 'core internal');
this._optimizeStandard = this._optimizeStandard.bind(this);
this._selectBulkhead = this._selectBulkhead.bind(this);
this.selectedRefId = null;
this.firstRefId = 'maxjump';
this.lastRefId = 'racer';
}
/**
* Handle focus if the component updates
* @param {Object} prevProps React Component properties
*/
componentDidUpdate(prevProps) {
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
constructor(props) {
super(props, 'core internal');
autoBind(this);
}
/**
* Use the lightest/optimal available standard modules
*/
_optimizeStandard() {
this.selectedRefId = 'maxjump';
this.props.ship.useLightestStandard();
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
@@ -50,12 +37,7 @@ export default class StandardSlotSection extends SlotSection {
* @param {integer} bulkheadIndex Bulkhead to use see Constants.BulkheadNames
*/
_multiPurpose(shielded, bulkheadIndex) {
this.selectedRefId = 'multipurpose';
if (bulkheadIndex === 2) this.selectedRefId = 'combat';
ShipRoles.multiPurpose(this.props.ship, shielded, bulkheadIndex);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
@@ -64,11 +46,7 @@ export default class StandardSlotSection extends SlotSection {
* @param {Boolean} shielded True if shield generator should be included
*/
_optimizeCargo(shielded) {
this.selectedRefId = 'trader';
ShipRoles.trader(this.props.ship, shielded);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
@@ -77,11 +55,7 @@ export default class StandardSlotSection extends SlotSection {
* @param {Boolean} shielded True if shield generator should be included
*/
_optimizeMiner(shielded) {
this.selectedRefId = 'miner';
ShipRoles.miner(this.props.ship, shielded);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
@@ -90,12 +64,7 @@ export default class StandardSlotSection extends SlotSection {
* @param {Boolean} planetary True if Planetary Vehicle Hangar (PVH) should be included
*/
_optimizeExplorer(planetary) {
this.selectedRefId = 'explorer';
if (planetary) this.selectedRefId = 'planetary';
ShipRoles.explorer(this.props.ship, planetary);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
@@ -103,22 +72,7 @@ export default class StandardSlotSection extends SlotSection {
* Racer role
*/
_optimizeRacer() {
this.selectedRefId = 'racer';
ShipRoles.racer(this.props.ship);
this.props.onChange();
this.props.onCargoChange(this.props.ship.cargoCapacity);
this.props.onFuelChange(this.props.ship.fuelCapacity);
this._close();
}
/**
* Use the specified bulkhead
* @param {Object} bulkhead Bulkhead module details
*/
_selectBulkhead(bulkhead) {
this.props.ship.useBulkhead(bulkhead.index);
this.context.tooltip();
this.props.onChange();
this._close();
}
@@ -129,113 +83,48 @@ export default class StandardSlotSection extends SlotSection {
this._optimizeStandard();
}
/**
* Creates a new slot for a given module.
* @param {Module} m Module to create the slot for
* @param {function} warning Warning callback
* @return {React.Component} Slot component
*/
_mkSlot(m, warning) {
const { currentMenu, propsToShow, onPropToggle } = this.props;
return <Slot key={m.getSlot()} m={m} warning={warning} hideSearch={true}
currentMenu={currentMenu} propsToShow={propsToShow} onPropToggle={onPropToggle}
/>;
}
/**
* Generate the slot React Components
* @return {Array} Array of Slots
*/
_getSlots() {
let { ship, currentMenu, cargo, fuel } = this.props;
let slots = new Array(8);
let open = this._openMenu;
let select = this._selectModule;
let st = ship.standard;
let avail = ship.getAvailableModules().standard;
let bh = ship.bulkheads;
slots[0] = <StandardSlot
key='bh'
slot={bh}
modules={ship.getAvailableModules().bulkheads}
onOpen={open.bind(this, bh)}
onSelect={this._selectBulkhead}
selected={currentMenu == bh}
onChange={this.props.onChange}
ship={ship}
/>;
slots[1] = <StandardSlot
key='pp'
slot={st[0]}
modules={avail[0]}
onOpen={open.bind(this, st[0])}
onSelect={select.bind(this, st[0])}
selected={currentMenu == st[0]}
onChange={this.props.onChange}
ship={ship}
warning={m => m instanceof Module ? m.getPowerGeneration() < ship.powerRetracted : m.pgen < ship.powerRetracted}
/>;
slots[2] = <StandardSlot
key='th'
slot={st[1]}
modules={avail[1]}
onOpen={open.bind(this, st[1])}
onSelect={select.bind(this, st[1])}
selected={currentMenu == st[1]}
onChange={this.props.onChange}
ship={ship}
warning={m => m instanceof Module ? m.getMaxMass() < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass) : m.maxmass < (ship.unladenMass + cargo + fuel - st[1].m.mass + m.mass)}
/>;
slots[3] = <StandardSlot
key='fsd'
slot={st[2]}
modules={avail[2]}
onOpen={open.bind(this, st[2])}
onSelect={select.bind(this, st[2])}
onChange={this.props.onChange}
ship={ship}
selected={currentMenu == st[2]}
/>;
slots[4] = <StandardSlot
key='ls'
slot={st[3]}
modules={avail[3]}
onOpen={open.bind(this, st[3])}
onSelect={select.bind(this, st[3])}
onChange={this.props.onChange}
ship={ship}
selected={currentMenu == st[3]}
/>;
slots[5] = <StandardSlot
key='pd'
slot={st[4]}
modules={avail[4]}
onOpen={open.bind(this, st[4])}
onSelect={select.bind(this, st[4])}
selected={currentMenu == st[4]}
onChange={this.props.onChange}
ship={ship}
warning={m => m instanceof Module ? m.getEnginesCapacity() <= ship.boostEnergy : m.engcap <= ship.boostEnergy}
/>;
slots[6] = <StandardSlot
key='ss'
slot={st[5]}
modules={avail[5]}
onOpen={open.bind(this, st[5])}
onSelect={select.bind(this, st[5])}
selected={currentMenu == st[5]}
onChange={this.props.onChange}
ship={ship}
/>;
slots[7] = <StandardSlot
key='ft'
slot={st[6]}
modules={avail[6]}
onOpen={open.bind(this, st[6])}
onSelect={select.bind(this, st[6])}
selected={currentMenu == st[6]}
onChange={this.props.onChange}
ship={ship}
warning= {m => m.fuel < st[2].m.maxfuel} // Show warning when fuel tank is smaller than FSD Max Fuel
/>;
return slots;
const { ship } = this.props;
const fsd = ship.getFSD();
return [
this._mkSlot(ship.getAlloys()),
this._mkSlot(
ship.getPowerPlant(),
(m) => moduleGet(m, 'powercapacity') < ship.get(CONSUMED_RETR),
),
this._mkSlot(
ship.getThrusters(),
(m) => moduleGet(m, 'enginemaximalmass') < ship.get(LADEN_MASS),
),
this._mkSlot(fsd),
this._mkSlot(
ship.getPowerDistributor(),
(m) => moduleGet(m, 'enginescapacity') <= ship.readProp('boostenergy'),
),
this._mkSlot(ship.getLifeSupport()),
this._mkSlot(ship.getSensors()),
this._mkSlot(
ship.getCoreFuelTank(),
(m) => moduleGet(m, 'fuel') < fsd.get('maxfuel')
),
];
}
/**
@@ -243,23 +132,22 @@ export default class StandardSlotSection extends SlotSection {
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) {
let planetaryDisabled = this.props.ship.internal.length < 4;
_getSectionMenu() {
const { translate } = this.context.language;
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
<ul>
<li className='lc' tabIndex="0" onClick={this._optimizeStandard} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['maxjump'] = smRef}>{translate('Maximize Jump Range')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeStandard}>{translate('Maximize Jump Range')}</li>
</ul>
<div className='select-group cap'>{translate('roles')}</div>
<ul>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, false, 0)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['multipurpose'] = smRef}>{translate('Multi-purpose')}</li>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, true, 2)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['combat'] = smRef}>{translate('Combat')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeCargo.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['trader'] = smRef}>{translate('Trader')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, false)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['explorer'] = smRef}>{translate('Explorer')}</li>
<li className={cn('lc', { disabled: planetaryDisabled })} tabIndex={planetaryDisabled ? '' : '0'} onClick={!planetaryDisabled && this._optimizeExplorer.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['planetary'] = smRef}>{translate('Planetary Explorer')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeMiner.bind(this, true)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['miner'] = smRef}>{translate('Miner')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeRacer.bind(this)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['racer'] = smRef}>{translate('Racer')}</li>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, false, 0)}>{translate('Multi-purpose')}</li>
<li className='lc' tabIndex="0" onClick={this._multiPurpose.bind(this, true, 2)}>{translate('Combat')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeCargo.bind(this, true)}>{translate('Trader')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, false)}>{translate('Explorer')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeExplorer.bind(this, true)}>{translate('Planetary Explorer')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeMiner.bind(this, true)}>{translate('Miner')}</li>
<li className='lc' tabIndex="0" onClick={this._optimizeRacer.bind(this)}>{translate('Racer')}</li>
</ul>
</div>;
}
}

View File

@@ -6,7 +6,6 @@ import TranslatedComponent from './TranslatedComponent';
* Document Root Tooltip
*/
export default class Tooltip extends TranslatedComponent {
static propTypes = {
rect: PropTypes.object.isRequired,
options: PropTypes.object
@@ -127,5 +126,4 @@ export default class Tooltip extends TranslatedComponent {
</div>
</div>;
}
}

View File

@@ -6,7 +6,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
* Abstract Translated Component
*/
export default class TranslatedComponent extends React.Component {
static contextTypes = {
language: PropTypes.object.isRequired,
sizeRatio: PropTypes.number.isRequired,

View File

@@ -1,7 +1,8 @@
import React from 'react';
import SlotSection from './SlotSection';
import HardpointSlot from './HardpointSlot';
import Slot from './Slot';
import { stopCtxPropagation } from '../utils/UtilityFunctions';
import autoBind from 'auto-bind';
/**
* Utility Slot Section
@@ -10,28 +11,16 @@ export default class UtilitySlotSection extends SlotSection {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props, context, 'utility', 'utility mounts');
this._empty = this._empty.bind(this);
this.selectedRefId = null;
this.firstRefId = 'emptyall';
this.lastRefId = 'po';
}
/**
* Handle focus if the component updates
* @param {Object} prevProps React Component properties
*/
componentDidUpdate(prevProps) {
this._handleSectionFocus(prevProps,this.firstRefId, this.lastRefId);
constructor(props) {
super(props, 'utility mounts');
autoBind(this);
}
/**
* Empty all utility slots and close the menu
*/
_empty() {
this.selectedRefId = this.firstRefId;
this.props.ship.emptyUtility();
this.props.onChange();
this._close();
@@ -45,9 +34,6 @@ export default class UtilitySlotSection extends SlotSection {
* @param {Synthetic} event Event
*/
_use(group, rating, name, event) {
this.selectedRefId = group;
if (rating !== null) this.selectedRefId += '-' + rating;
this.props.ship.useUtility(group, rating, name, event.getModifierState('Alt'));
this.props.onChange();
this._close();
@@ -66,32 +52,25 @@ export default class UtilitySlotSection extends SlotSection {
*/
_getSlots() {
let slots = [];
let { ship, currentMenu } = this.props;
let hardpoints = ship.hardpoints;
let { ship, currentMenu, propsToShow, onPropToggle } = this.props;
let { originSlot, targetSlot } = this.state;
let availableModules = ship.getAvailableModules();
for (let i = 0, l = hardpoints.length; i < l; i++) {
let h = hardpoints[i];
if (h.maxClass === 0) {
slots.push(<HardpointSlot
key={i}
maxClass={h.maxClass}
availableModules={() => availableModules.getHps(h.maxClass)}
onOpen={this._openMenu.bind(this,h)}
onSelect={this._selectModule.bind(this, h)}
for (let h of ship.getUtilities(undefined, true)) {
slots.push(<Slot
key={h.object.Slot}
maxClass={h.getSize()}
onChange={this.props.onChange}
selected={currentMenu == h}
currentMenu={currentMenu}
drag={this._drag.bind(this, h)}
dragOver={this._dragOverSlot.bind(this, h)}
drop={this._drop}
dropClass={this._dropClass(h, originSlot, targetSlot)}
ship={ship}
m={h.m}
m={h}
enabled={h.enabled ? true : false}
propsToShow={propsToShow}
onPropToggle={onPropToggle}
/>);
}
}
return slots;
}
@@ -101,33 +80,34 @@ export default class UtilitySlotSection extends SlotSection {
* @param {Function} translate Translate function
* @return {React.Component} Section menu
*/
_getSectionMenu(translate) {
_getSectionMenu() {
const { translate } = this.context.language;
let _use = this._use;
return <div className='select' onClick={(e) => e.stopPropagation()} onContextMenu={stopCtxPropagation}>
<ul>
<li className='lc' tabIndex='0' onClick={this._empty} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['emptyall'] = smRef}>{translate('empty all')}</li>
<li className='lc' tabIndex='0' onClick={this._empty}>{translate('empty all')}</li>
<li className='optional-hide' style={{ textAlign: 'center', marginTop: '1em' }}>{translate('PHRASE_ALT_ALL')}</li>
</ul>
<div className='select-group cap'>{translate('sb')}</div>
<ul>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'A', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-A'] = smRef}>A</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'B', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-B'] = smRef}>B</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'C', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-C'] = smRef}>C</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'D', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-D'] = smRef}>D</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'E', null)} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['sb-E'] = smRef}>E</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'A', null)}>A</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'B', null)}>B</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'C', null)}>C</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'D', null)}>D</li>
<li className='c' tabIndex='0' onClick={_use.bind(this, 'sb', 'E', null)}>E</li>
</ul>
<div className='select-group cap'>{translate('hs')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'hs', null, 'Heat Sink Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['hs'] = smRef}>{translate('Heat Sink Launcher')}</li>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'hs', null, 'Heat Sink Launcher')}>{translate('Heat Sink Launcher')}</li>
</ul>
<div className='select-group cap'>{translate('ch')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'ch', null, 'Chaff Launcher')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['ch'] = smRef}>{translate('Chaff Launcher')}</li>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'ch', null, 'Chaff Launcher')}>{translate('Chaff Launcher')}</li>
</ul>
<div className='select-group cap'>{translate('po')}</div>
<ul>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'po', null, 'Point Defence')} onKeyDown={this._keyDown} ref={smRef => this.sectionRefArr['po'] = smRef}>{translate('Point Defence')}</li>
<li className='lc' tabIndex='0' onClick={_use.bind(this, 'po', null, 'Point Defence')}>{translate('Point Defence')}</li>
</ul>
</div>;
}

View File

@@ -3,193 +3,99 @@ import PropTypes from 'prop-types';
import TranslatedComponent from './TranslatedComponent';
import LineChart from '../components/LineChart';
import * as Calc from '../shipyard/Calculations';
import { moduleReduce } from 'ed-forge/lib/helper';
import { chain, keys, mapValues, values } from 'lodash';
const DAMAGE_DEALT_COLORS = ['#FFFFFF', '#FF0000', '#00FF00', '#7777FF', '#FFFF00', '#FF00FF', '#00FFFF', '#777777'];
const PORTION_MAPPINGS = {
'absolute': 'absolutedamageportion',
'explosive': 'explosivedamageportion',
'kinetic': 'kineticdamageportion',
'thermal': 'thermicdamageportion',
};
const MULTS = keys(PORTION_MAPPINGS);
// TODO: help with this in ed-forge
/**
* .
* @param {Object} opponentDefence .
* @returns {Object} .
*/
function defenceToMults(opponentDefence) {
return chain(opponentDefence)
.pick(MULTS)
.mapKeys((v, k) => PORTION_MAPPINGS[k])
.mapValues((resistanceProfile) => resistanceProfile.damageMultiplier)
.value();
}
/**
* Weapon damage chart
*/
export default class WeaponDamageChart extends TranslatedComponent {
static propTypes = {
code: PropTypes.string.isRequired,
ship: PropTypes.object.isRequired,
opponent: PropTypes.object.isRequired,
hull: PropTypes.bool.isRequired,
opponentDefence: PropTypes.object.isRequired,
engagementRange: PropTypes.number.isRequired,
opponentSys: PropTypes.number.isRequired,
marker: PropTypes.string.isRequired
};
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
}
/**
* Set the initial weapons state
*/
componentWillMount() {
const weaponNames = this._weaponNames(this.props.ship, this.context);
const opponentShields = Calc.shieldMetrics(this.props.opponent, this.props.opponentSys);
const opponentArmour = Calc.armourMetrics(this.props.opponent);
const maxRange = this._calcMaxRange(this.props.ship);
const maxDps = this._calcMaxSDps(this.props.ship, this.props.opponent, opponentShields, opponentArmour);
this.setState({ maxRange, maxDps, weaponNames, opponentShields, opponentArmour, calcSDpsFunc: this._calcSDps.bind(this, this.props.ship, weaponNames, this.props.opponent, opponentShields, opponentArmour, this.props.hull) });
}
/**
* Set the updated weapons state if our ship changes
* @param {Object} nextProps Incoming/Next properties
* @param {Object} nextContext Incoming/Next conext
* @return {boolean} Returns true if the component should be rerendered
*/
componentWillReceiveProps(nextProps, nextContext) {
if (nextProps.marker != this.props.marker) {
const weaponNames = this._weaponNames(nextProps.ship, nextContext);
const opponentShields = Calc.shieldMetrics(nextProps.opponent, nextProps.opponentSys);
const opponentArmour = Calc.armourMetrics(nextProps.opponent);
const maxRange = this._calcMaxRange(nextProps.ship);
const maxDps = this._calcMaxSDps(nextProps.ship, nextProps.opponent, opponentShields, opponentArmour);
this.setState({ weaponNames,
opponentShields,
opponentArmour,
maxRange,
maxDps,
calcSDpsFunc: this._calcSDps.bind(this, nextProps.ship, weaponNames, nextProps.opponent, opponentShields, opponentArmour, nextProps.hull)
});
}
return true;
}
/**
* 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; // Minimum
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;
}
/**
* Calculate the maximum sustained single-weapon DPS for this ship
* @param {Object} ship The ship
* @param {Object} opponent The opponent ship
* @param {Object} opponentShields The opponent's shields
* @param {Object} opponentArmour The opponent's armour
* @return {number} The maximum sustained single-weapon DPS
*/
_calcMaxSDps(ship, opponent, opponentShields, opponentArmour) {
// Additional information to allow effectiveness calculations
let maxSDps = 0;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, 0);
const thisSDps = sustainedDps.damage.armour.total > sustainedDps.damage.shields.total ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
if (thisSDps > maxSDps) {
maxSDps = thisSDps;
}
}
}
return maxSDps;
}
/**
* Obtain the weapon names for this ship
* @param {Object} ship The ship
* @param {Object} context The context
* @return {array} The weapon names
*/
_weaponNames(ship, context) {
const translate = context.language.translate;
let names = [];
let num = 1;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
let name = '' + num++ + ': ' + m.class + m.rating + (m.missile ? '/' + m.missile : '') + ' ' + translate(m.name || m.grp);
let engineering;
if (m.blueprint && m.blueprint.name) {
engineering = translate(m.blueprint.name) + ' ' + translate('grade') + ' ' + m.blueprint.grade;
if (m.blueprint.special && m.blueprint.special.id) {
engineering += ', ' + translate(m.blueprint.special.name);
}
}
if (engineering) {
name = name + ' (' + engineering + ')';
}
names.push(name);
}
}
return names;
}
/**
* Calculate the per-weapon sustained DPS for this ship against another ship at a given range
* @param {Object} ship The ship
* @param {Object} weaponNames The names of the weapons for which to calculate DPS
* @param {Object} opponent The target
* @param {Object} opponentShields The opponent's shields
* @param {Object} opponentArmour The opponent's armour
* @param {bool} hull true if to calculate against hull, false if to calculate against shields
* @param {Object} engagementRange The engagement range
* @return {array} The array of weapon DPS
*/
_calcSDps(ship, weaponNames, opponent, opponentShields, opponentArmour, hull, engagementRange) {
let results = {};
let weaponNum = 0;
for (let i = 0; i < ship.hardpoints.length; i++) {
if (ship.hardpoints[i].maxClass > 0 && ship.hardpoints[i].m && ship.hardpoints[i].enabled) {
const m = ship.hardpoints[i].m;
const sustainedDps = Calc._weaponSustainedDps(m, opponent, opponentShields, opponentArmour, engagementRange);
results[weaponNames[weaponNum++]] = hull ? sustainedDps.damage.armour.total : sustainedDps.damage.shields.total;
}
}
return results;
}
/**
* Render damage dealt
* @return {React.Component} contents
*/
render() {
const { language, onWindowResize, sizeRatio, tooltip, termtip } = this.context;
const { formats, translate, units } = language;
const { maxRange } = this.state;
const { ship, opponent, engagementRange } = this.props;
const { language } = this.context;
const { translate } = language;
const { code, ship, opponentDefence, engagementRange } = this.props;
const sortOrder = this._sortOrder;
const onCollapseExpand = this._onCollapseExpand;
const code = `${ship.toString()}:${opponent.toString()}`;
const hardpoints = ship.getHardpoints();
const hardpointsMap = chain(hardpoints)
.map((m) => [m.getSlot(), m])
.fromPairs()
.value();
const mults = defenceToMults(opponentDefence);
const cb = (range) => {
return mapValues(
hardpointsMap,
(m) => {
const sdps = m.get('sustaineddamagepersecond', true);
const resistanceMul = chain(mults)
.toPairs()
.map((pair) => {
const [k, mul] = pair;
return m.get(k, true) * mul;
})
.sum()
.value();
const falloff = m.get('damagefalloffrange', true);
const rangeMul = Math.min(1, Math.max(0,
1 - (range - falloff) / (m.get('maximumrange', true) - falloff)
));
return sdps * resistanceMul * rangeMul;
}
);
};
return (
<div>
<LineChart
xMax={maxRange}
yMax={this.state.maxDps}
xMin={0}
xMax={moduleReduce(
hardpoints, 'maximumrange', true, (a, v) => Math.max(a, v), 1000,
)}
yMin={0}
// Factor in highest damage multiplier to get a safe upper bound
yMax={Math.max(1, ...values(mults)) * moduleReduce(
hardpoints, 'sustaineddamagepersecond', true, (a, v) => Math.max(a, v), 0,
)}
xLabel={translate('range')}
xUnit={translate('m')}
yLabel={translate('sdps')}
series={this.state.weaponNames}
xMark={this.props.engagementRange}
yLabel={translate('sustaineddamagepersecond')}
series={hardpoints.map((m) => m.getSlot())}
xMark={engagementRange}
colors={DAMAGE_DEALT_COLORS}
func={this.state.calcSDpsFunc}
func={cb}
points={200}
code={code}
/>

View File

@@ -5,7 +5,6 @@ import PropTypes from 'prop-types';
* Unexpected Error page / block
*/
export default class ErrorDetails extends React.Component {
static contextTypes = {
route: PropTypes.object.isRequired,
language: PropTypes.object.isRequired

View File

@@ -6,7 +6,8 @@ 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 { Factory, Ship } from 'ed-forge';
import { STATE_EVENT, OBJECT_EVENT } from 'ed-forge/lib/Ship';
import * as _ from 'lodash';
import { toDetailedBuild } from '../shipyard/Serializer';
import { outfitURL } from '../utils/UrlGenerators';
@@ -38,16 +39,77 @@ import ModalExport from '../components/ModalExport';
import ModalPermalink from '../components/ModalPermalink';
import ModalShoppingList from '../components/ModalShoppingList';
import ModalOrbis from '../components/ModalOrbis';
import autoBind from 'auto-bind';
import { assign } from 'lodash';
/**
* 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;
}
const SHOW_BY_DEFAULT = {
'cabincapacity': true,
'causticresistance': true,
'explosiveresistance': true,
'kineticresistance': true,
'thermicresistance': true,
'heatefficiency': true,
'powercapacity': true,
'integrity': true,
'engineminimalmass': true,
'engineoptimalmass': true,
'enginemaximalmass': true,
'engineoptperformance': true,
'fsdoptimalmass': true,
'maxfuel': true,
'dronelifetime': true,
'weaponscapacity': true,
'weaponsrecharge': true,
'systemscapacity': true,
'systemsrecharge': true,
'enginescapacity': true,
'enginesrecharge': true,
'range': true,
'shieldgenmaximalmass': true,
'shieldgenminimalmass': true,
'shieldgenoptimalmass': true,
'brokenregenrate': true,
'regenrate': true,
'shieldgenstrength': true,
'ammomaximum': true,
'afmrepaircapacity': true,
'fsdinterdictorfacinglimit': true,
'fuelscooprate': true,
'bays': true,
'rebuildsperbay': true,
'maximumrange': true,
'maxactivedrones': true,
'refinerybins': true,
'shieldbankduration': true,
'shieldbankspinup': true,
'shieldbankreinforcement': true,
'defencemodifierhealthaddition': true,
'protection': true,
'dronehackingtime': true,
'defencemodifiershieldaddition': true,
'jumpboost': true,
'damagepersecond': true,
'damage': true,
'energypersecond': true,
'heatpersecond': true,
'sustaineddamagepersecond': true,
'damageperenergy': true,
'rateoffire': true,
'maximumrange': true,
'damagefalloffrange': true,
'armourpenetration': true,
'sustainedenergypersecond': true,
'sustainedheatpersecond': true,
'ammoclipsize': true,
'ammomaximum': true,
'reloadtime': true,
'shotspeed': true,
'defencemodifiershieldmultiplier': true,
'scannerrange': true,
'scannertimetoscan': true,
'maxangle': true,
'mass': true,
};
/**
* The Outfitting Page
@@ -60,17 +122,8 @@ export default class OutfittingPage extends Page {
*/
constructor(props, context) {
super(props, context);
// window.Perf = Perf;
autoBind(this);
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 = {};
}
/**
@@ -82,68 +135,36 @@ export default class OutfittingPage extends Page {
_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
}
let code = params.code || savedCode;
// Create a new Ship instance
const ship = code ? new Ship(code) : Factory.newShip(shipId);
ship.on(STATE_EVENT, this._shipUpdated);
ship.on(OBJECT_EVENT, this._shipUpdated);
this._getTitle = getTitle.bind(this, data.properties.name);
// Obtain ship control from code
const {
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
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,
code: ship.compress(),
savedCode,
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
opponentSys,
opponentEng,
opponentWep,
engagementRange
propsToShow: assign({}, SHOW_BY_DEFAULT),
};
}
/**
* Get this pages title for the browser.
* @returns {string} Page title
*/
_getTitle() {
const { buildName } = this.state;
const { translate } = this.context.language;
return buildName || translate(this.ship.getShipType());
}
/**
* Handle build name change and update state
* @param {SyntheticEvent} event React Event
@@ -153,10 +174,12 @@ export default class OutfittingPage extends Page {
newBuildName: event.target.value
};
if (Persist.hasBuild(this.state.shipId, stateChanges.newBuildName)) {
const { ship } = this.state;
const shipId = ship.getShipType();
if (Persist.hasBuild(shipId, stateChanges.newBuildName)) {
stateChanges.savedCode = Persist.getBuild(
this.state.shipId,
stateChanges.newBuildName
shipId,
stateChanges.newBuildName,
);
} else {
stateChanges.savedCode = null;
@@ -168,170 +191,13 @@ export default class OutfittingPage extends Page {
/**
* Update the control part of the route
*/
_updateRouteOnControlChange() {
const { ship, shipId, buildName } = this.state;
const code = this._fullCode(ship);
this._updateRoute(shipId, buildName, code);
_updateRoute() {
const { ship } = this.state;
const code = ship.compress();
this._setRoute();
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 mcSys = 0;
let mcEng = 0;
let mcWep = 0;
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]);
if (sys + eng + wep > 6) {
sys = eng = wep = 2;
}
boost = control[3] == 1 ? true : false;
fuel = parseFloat(control[4]) || fuel;
cargo = parseInt(control[5]) || cargo;
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]) || opponentSys;
opponentEng = parseFloat(opponentControl[1]) || opponentEng;
opponentWep = parseFloat(opponentControl[2]) || opponentWep;
}
}
} else {
// Ship is a stock build
opponent.buildWith(Ships[shipId].defaults);
}
}
engagementRange = parseInt(control[8]) || engagementRange;
// Multi-crew pips were introduced later on so assign default values
// because those values might not be present.
mcSys = parseInt(control[9]) || mcSys;
mcEng = parseInt(control[10]) || mcEng;
mcWep = parseInt(control[11]) || mcWep;
}
}
return {
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
opponentSys,
opponentEng,
opponentWep,
engagementRange
};
}
/**
* Triggered when pips have been updated. Multi-crew pips are already included
* in sys, eng and wep but mcSys, mcEng and mcWep make clear where each pip
* comes from.
* @param {number} sys SYS pips
* @param {number} eng ENG pips
* @param {number} wep WEP pips
* @param {number} mcSys SYS pips from multi-crew
* @param {number} mcEng ENG pips from multi-crew
* @param {number} mcWep WEP pips from multi-crew
*/
_pipsUpdated(sys, eng, wep, mcSys, mcEng, mcWep) {
this.setState({ sys, eng, wep, mcSys, mcEng, mcWep }, () =>
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
@@ -387,47 +253,35 @@ export default class OutfittingPage extends Page {
opponentEng,
opponentWep
},
() => this._updateRouteOnControlChange()
() => this._updateRoute()
);
}
/**
* 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,
mcSys,
mcEng,
mcWep,
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}/${mcSys}/${mcEng}/${mcWep}`;
return code;
_propToShowToggled(propertyName, newStatus) {
const { propsToShow } = this.state;
if (newStatus === propsToShow[propertyName]) {
return;
}
if (newStatus) {
propsToShow[propertyName] = true;
} else {
delete propsToShow[propertyName];
}
this.setState({ propsToShow: assign({}, propsToShow) });
}
/**
* Save the current build
*/
_saveBuild() {
const { ship, buildName, newBuildName, shipId } = this.state;
const { ship, buildName, newBuildName } = this.state;
const shipId = ship.getShipType();
// 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();
const code = this.state.code || ship.compress();
Persist.saveBuild(shipId, newBuildName, code);
this._updateRoute(shipId, newBuildName, code);
this._setRoute();
let opponent, opponentBuild, opponentSys, opponentEng, opponentWep;
if (
@@ -460,7 +314,6 @@ export default class OutfittingPage extends Page {
opponentSys,
opponentEng,
opponentWep,
title: this._getTitle(newBuildName)
});
}
@@ -468,11 +321,12 @@ export default class OutfittingPage extends Page {
* Rename the current build
*/
_renameBuild() {
const { code, buildName, newBuildName, shipId, ship } = this.state;
const { code, buildName, newBuildName, ship } = this.state;
const shipId = ship.getShipType();
if (buildName != newBuildName && newBuildName.length) {
Persist.deleteBuild(shipId, buildName);
Persist.saveBuild(shipId, newBuildName, code);
this._updateRoute(shipId, newBuildName, code);
this._setRoute();
this.setState({
buildName: newBuildName,
code,
@@ -493,50 +347,19 @@ export default class OutfittingPage extends Page {
* Reset build to Stock/Factory defaults
*/
_resetBuild() {
const { ship, shipId, buildName } = this.state;
let { ship } = this.state;
// Rebuild ship
ship.buildWith(Ships[shipId].defaults);
// Reset controls
const code = ship.toString();
const {
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
} = this._obtainControlFromCode(ship, code);
ship = Factory.newShip(ship.getShipType());
// Update state, and refresh the ship
this.setState(
{
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
},
() => this._updateRoute(shipId, buildName, code)
);
this.setState({ ship, code: undefined }, () => this._setRoute());
}
/**
* Delete the build
*/
_deleteBuild() {
const { shipId, buildName } = this.state;
const { ship, buildName } = this.state;
const shipId = ship.getShipType();
Persist.deleteBuild(shipId, buildName);
let opponentBuild;
@@ -549,7 +372,7 @@ export default class OutfittingPage extends Page {
} else {
opponentBuild = this.state.opponentBuild;
}
Router.go(outfitURL(this.state.shipId));
Router.go(outfitURL(shipId));
this.setState({ opponentBuild });
}
@@ -573,43 +396,12 @@ export default class OutfittingPage extends Page {
* 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;
const { ship, code, buildName } = this.state;
const shipId = ship.getShipType();
// Rebuild ship from the code
this.state.ship.buildFrom(code);
// Obtain controls from the code
const {
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
} = this._obtainControlFromCode(ship, code);
// Update state, and refresh the route when complete
this.setState(
{
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
engagementRange
},
() => this._updateRoute(shipId, buildName, code)
{ ship: new Ship(code), },
() => this._setRoute(),
);
}
@@ -617,23 +409,11 @@ export default class OutfittingPage extends Page {
* 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);
let { ship } = this.state;
const code = ship.compress();
// 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)
);
if (this.state.code != code) {
this.setState({ code }, () => this._setRoute());
}
}
@@ -643,8 +423,9 @@ export default class OutfittingPage extends Page {
* @param {string} buildName Current build name
* @param {string} code Serialized ship 'code'
*/
_updateRoute(shipId, buildName, code) {
Router.replace(outfitURL(shipId, code, buildName));
_setRoute() {
const { ship, code, buildName } = this.state;
Router.replace(outfitURL(ship.getShipType(), code, buildName));
}
/**
@@ -747,123 +528,93 @@ export default class OutfittingPage extends Page {
*/
renderPage() {
let state = this.state,
{ language, termtip, tooltip, sizeRatio, onWindowResize } = this.context,
{ translate, units, formats } = language,
{ language, termtip, tooltip, sizeRatio } = this.context,
{ translate } = language,
{
ship,
code,
savedCode,
buildName,
newBuildName,
sys,
eng,
wep,
mcSys,
mcEng,
mcWep,
boost,
fuel,
cargo,
opponent,
opponentBuild,
opponentSys,
opponentEng,
opponentWep,
engagementRange
// 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 || '');
// 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(
// <div
// key={textKey}
// className={className}
// onMouseEnter={termtip.bind(null, tooltipTextKey)}
// onMouseLeave={hide}
// >
// <a
// href={
// textKey.startsWith('empire') ?
// 'http://elite-dangerous.wikia.com/wiki/Empire/Ranks' :
// 'http://elite-dangerous.wikia.com/wiki/Federation/Ranks'
// }
// target="_blank"
// rel="noopener"
// >
// {translate(textKey)}
// </a>
// </div>
// );
// } else {
// requirementElements.push(
// <div
// key={textKey}
// className={className}
// onMouseEnter={termtip.bind(null, tooltipTextKey)}
// onMouseLeave={hide}
// >
// {translate(textKey)}
// </div>
// );
// }
// }
// 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(
<div
key={textKey}
className={className}
onMouseEnter={termtip.bind(null, tooltipTextKey)}
onMouseLeave={hide}
>
<a
href={
textKey.startsWith('empire') ?
'http://elite-dangerous.wikia.com/wiki/Empire/Ranks' :
'http://elite-dangerous.wikia.com/wiki/Federation/Ranks'
}
target="_blank"
rel="noopener"
>
{translate(textKey)}
</a>
</div>
);
} else {
requirementElements.push(
<div
key={textKey}
className={className}
onMouseEnter={termtip.bind(null, tooltipTextKey)}
onMouseLeave={hide}
>
{translate(textKey)}
</div>
);
}
}
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'
);
}
// 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 (
<div
@@ -872,8 +623,8 @@ export default class OutfittingPage extends Page {
style={{ fontSize: sizeRatio * 0.9 + 'em' }}
>
<div id="overview">
<h1>{ship.name}</h1>
<div id="requirements">{requirementElements}</div>
<h1>{ship.getShipType()}</h1>
{/* <div id="requirements">{requirementElements}</div> */}
<div id="build">
<input
value={newBuildName || ''}
@@ -933,7 +684,7 @@ export default class OutfittingPage extends Page {
<Download className="lg" />
</button>
<button
onClick={this._eddbShoppingList}
// onClick={this._eddbShoppingList}
onMouseOver={termtip.bind(null, 'PHRASE_SHOPPING_LIST')}
onMouseOut={hide}
>
@@ -954,7 +705,7 @@ export default class OutfittingPage extends Page {
<OrbisIcon className="lg" />
</button>
<button
onClick={this._genShoppingList}
// onClick={this._genShoppingList}
onMouseOver={termtip.bind(null, 'PHRASE_SHOPPING_MATS')}
onMouseOut={hide}
>
@@ -964,58 +715,18 @@ export default class OutfittingPage extends Page {
</div>
{/* Main tables */}
<ShipSummaryTable
ship={ship}
fuel={fuel}
cargo={cargo}
marker={shipSummaryMarker}
pips={{
sys: this.state.sys,
wep: this.state.wep,
eng: this.state.eng
}}
/>
<StandardSlotSection
ship={ship}
fuel={fuel}
cargo={cargo}
code={standardSlotMarker}
onChange={shipUpdated}
onCargoChange={this._cargoUpdated}
onFuelChange={this._fuelUpdated}
currentMenu={menu}
sectionMenuRefs={this._sectionMenuRefs}
/>
<InternalSlotSection
ship={ship}
code={internalSlotMarker}
onChange={shipUpdated}
onCargoChange={this._cargoUpdated}
onFuelChange={this._fuelUpdated}
currentMenu={menu}
sectionMenuRefs={this._sectionMenuRefs}
/>
<HardpointSlotSection
ship={ship}
code={hardpointsSlotMarker}
onChange={shipUpdated}
onCargoChange={this._cargoUpdated}
onFuelChange={this._fuelUpdated}
currentMenu={menu}
sectionMenuRefs={this._sectionMenuRefs}
/>
<UtilitySlotSection
ship={ship}
code={hardpointsSlotMarker}
onChange={shipUpdated}
onCargoChange={this._cargoUpdated}
onFuelChange={this._fuelUpdated}
currentMenu={menu}
sectionMenuRefs={this._sectionMenuRefs}
/>
<ShipSummaryTable ship={ship} code={code} />
<StandardSlotSection ship={ship} code={code} currentMenu={menu}
propsToShow={propsToShow} onPropToggle={this._propToShowToggled} />
<InternalSlotSection ship={ship} code={code} currentMenu={menu}
propsToShow={propsToShow} onPropToggle={this._propToShowToggled} />
<HardpointSlotSection ship={ship} code={code} currentMenu={menu}
propsToShow={propsToShow} onPropToggle={this._propToShowToggled} />
<UtilitySlotSection ship={ship} code={code} currentMenu={menu}
propsToShow={propsToShow} onPropToggle={this._propToShowToggled} />
{/* Control of ship and opponent */}
<div className="group quarter">
{/* <div className="group quarter">
<div className="group half">
<h2 style={{ verticalAlign: 'middle', textAlign: 'left' }}>
{translate('ship control')}
@@ -1044,7 +755,6 @@ export default class OutfittingPage extends Page {
<div className="group quarter">
<Fuel
fuelCapacity={ship.fuelCapacity}
fuel={fuel}
onChange={this._fuelUpdated}
/>
</div>
@@ -1052,7 +762,6 @@ export default class OutfittingPage extends Page {
{ship.cargoCapacity > 0 ? (
<Cargo
cargoCapacity={ship.cargoCapacity}
cargo={cargo}
onChange={this._cargoUpdated}
/>
) : null}
@@ -1077,10 +786,10 @@ export default class OutfittingPage extends Page {
engagementRange={engagementRange}
onChange={this._engagementRangeUpdated}
/>
</div>
</div> */}
{/* Tabbed subpages */}
<OutfittingSubpages
{/* <OutfittingSubpages
ship={ship}
code={code}
buildName={buildName}
@@ -1097,7 +806,7 @@ export default class OutfittingPage extends Page {
opponentSys={opponentSys}
opponentEng={opponentEng}
opponentWep={opponentWep}
/>
/> */}
</div>
);
}

View File

@@ -7,7 +7,6 @@ import { shallowEqual } from '../utils/UtilityFunctions';
* Abstract/Base Page
*/
export default class Page extends React.Component {
static contextTypes = {
closeMenu: PropTypes.func.isRequired,
hideModal: PropTypes.func.isRequired,
@@ -84,5 +83,4 @@ export default class Page extends React.Component {
}
return this.renderPage();
}
}

View File

@@ -1,102 +1,81 @@
import React from 'react';
import Page from './Page';
import { Ships } from 'coriolis-data/dist';
import cn from 'classnames';
import Ship from '../shipyard/Ship';
import * as ModuleUtils from '../shipyard/ModuleUtils';
import { Factory } from 'ed-forge';
import { JUMP_METRICS } from 'ed-forge/lib/ship-stats';
import { SizeMap } from '../shipyard/Constants';
import Link from '../components/Link';
/**
* Counts the hardpoints by class/size
* @param {Object} slot Hardpoint Slot model
*/
function countHp(slot) {
this.hp[slot.maxClass]++;
this.hpCount++;
}
/**
* Counts the internal slots and aggregated properties
* @param {Object} slot Internal Slots
*/
function countInt(slot) {
let crEligible = !slot.eligible || slot.eligible.cr;
this.int[slot.maxClass - 1]++; // Subtract 1 since there is no Class 0 Internal compartment
this.intCount++;
this.maxCargo += crEligible ?
ModuleUtils.findInternal('cr', slot.maxClass, 'E').cargo :
0;
// if no eligiblity, then assume pce
let passSlotType = null;
let passSlotRating = null;
if (!slot.eligible || slot.eligible.pce) {
passSlotType = 'pce';
passSlotRating = 'E';
} else if (slot.eligible.pci) {
passSlotType = 'pci';
passSlotRating = 'D';
} else if (slot.eligible.pcm) {
passSlotType = 'pcm';
passSlotRating = 'C';
} else if (slot.eligible.pcq) {
passSlotType = 'pcq';
passSlotRating = 'B';
}
let passengerBay = passSlotType ?
ModuleUtils.findMaxInternal(passSlotType, slot.maxClass, passSlotRating) :
null;
this.maxPassengers += passengerBay ? passengerBay.passengers : 0;
}
/**
* Generate Ship summary and aggregated properties
* @param {String} shipId Ship Id
* @param {Object} shipData Ship Default Data
* @return {Object} Ship summary and aggregated properties
*/
function shipSummary(shipId, shipData) {
function shipSummary(shipId) {
// Build Ship
let ship = Factory.newShip(shipId);
let coreSizes = ship.readMeta('coreSizes');
let { jumpRange, totalRange } = ship.getMetrics(JUMP_METRICS);
let summary = {
agility: ship.readProp('pitch') + ship.readProp('yaw') + ship.readProp('roll'),
baseArmour: ship.readProp('basearmour'),
baseShieldStrength: ship.readProp('baseshieldstrength'),
boost: ship.readProp('boost'),
class: ship.readMeta('class'),
crew: ship.readMeta('crew'),
id: shipId,
hardness: ship.readProp('hardness'),
hpCount: 0,
hullMass: ship.readProp('hullmass'),
intCount: 0,
beta: shipData.beta,
maxCargo: 0,
maxPassengers: 0,
masslock: ship.readProp('masslock'),
hp: [0, 0, 0, 0, 0], // Utility, Small, Medium, Large, Huge
int: [0, 0, 0, 0, 0, 0, 0, 0], // Sizes 1 - 8
standard: shipData.slots.standard,
agility:
shipData.properties.pitch +
shipData.properties.yaw +
shipData.properties.roll
jumpRange,
pitch: ship.readProp('pitch'),
retailCost: ship.readMeta('retailCost'),
roll: ship.readProp('roll'),
speed: ship.readProp('speed'),
standard: [
'powerplant',
'mainengines',
'frameshiftdrive',
'lifesupport',
'powerdistributor',
'radar',
'fueltank'
].map(k => coreSizes[k]),
totalRange,
yaw: ship.readProp('yaw'),
};
Object.assign(summary, shipData.properties);
let ship = new Ship(shipId, shipData.properties, shipData.slots);
// Build Ship
ship.buildWith(shipData.defaults); // Populate with stock/default components
ship.hardpoints.forEach(countHp.bind(summary)); // Count Hardpoints by class
ship.internal.forEach(countInt.bind(summary)); // Count Internal Compartments by class
summary.retailCost = ship.totalCost; // Record Stock/Default/retail cost
ship.optimizeMass({ pd: '1D' }); // Optimize Mass with 1D PD for maximum possible jump range
summary.maxJumpRange = ship.unladenRange; // Record Jump Range
// Count Hardpoints by class
ship.getHardpoints(undefined, true).forEach(hardpoint => {
summary.hp[hardpoint.getSize()]++;
summary.hpCount++;
});
// Count Internal Compartments by class
let maxCargo = 0, maxPassengers = 0;
ship.getInternals(undefined, true).forEach(internal => {
const size = String(internal.getSize());
summary.int[size]++;
summary.intCount++;
// Best thrusters
let th;
if (ship.standard[1].maxClass === 3) {
th = 'tz';
} else if (ship.standard[1].maxClass === 2) {
th = 'u0';
} else {
th = ship.standard[1].maxClass + 'A';
}
// Try cargo racks
try {
internal.setItem('cargorack', size);
maxCargo += internal.get('cargo');
} catch {}
// Try economy cabins
try {
internal.setItem('passengercabins', size < '6' ? size : '6', '1');
maxPassengers += internal.get('cabincapacity');
} catch {}
});
ship.optimizeMass({ th, fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters
summary.topSpeed = ship.topSpeed;
summary.topBoost = ship.topBoost;
summary.baseArmour = ship.armour;
summary.maxCargo = maxCargo;
summary.maxPassengers = maxPassengers;
return summary;
}
@@ -117,14 +96,14 @@ export default class ShipyardPage extends Page {
if (!ShipyardPage.cachedShipSummaries) {
ShipyardPage.cachedShipSummaries = [];
for (let s in Ships) {
ShipyardPage.cachedShipSummaries.push(shipSummary(s, Ships[s]));
for (let s of Factory.getAllShipTypes()) {
ShipyardPage.cachedShipSummaries.push(shipSummary(s));
}
}
this.state = {
title: 'Coriolis EDCD Edition - Shipyard',
shipPredicate: 'name',
shipPredicate: 'id',
shipDesc: true,
shipSummaries: ShipyardPage.cachedShipSummaries,
compare: {},
@@ -157,7 +136,7 @@ export default class ShipyardPage extends Page {
* @private
*/
_toggleGroupCompared() {
this.setState({groupCompared: !this.state.groupCompared})
this.setState({ groupCompared: !this.state.groupCompared });
}
/**
@@ -205,21 +184,22 @@ export default class ShipyardPage extends Page {
onMouseEnter={noTouch && this._highlightShip.bind(this, s.id)}
onClick={() => this._toggleCompare(s.id)}
>
<td className="ri">{s.manufacturer}</td>
<td className="ri">{fInt(s.retailCost)}</td>
<td className="ri cap">{translate(SizeMap[s.class])}</td>
<td className="ri">{fInt(s.crew)}</td>
<td className="ri">{s.masslock}</td>
<td className="ri">{fInt(s.agility)}</td>
<td className="ri">{fInt(s.hardness)}</td>
<td className="ri">{fInt(s.hullMass)}</td>
<td className="ri">{fInt(s.agility)}</td>
<td className="ri">{fInt(s.speed)}</td>
<td className="ri">{fInt(s.boost)}</td>
<td className="ri">{fInt(s.pitch)}</td>
<td className="ri">{fInt(s.yaw)}</td>
<td className="ri">{fInt(s.roll)}</td>
<td className="ri">{fRound(s.jumpRange)}</td>
<td className="ri">{fRound(s.totalRange)}</td>
<td className="ri">{fInt(s.hardness)}</td>
<td className="ri">{fInt(s.baseArmour)}</td>
<td className="ri">{fInt(s.baseShieldStrength)}</td>
<td className="ri">{fInt(s.topSpeed)}</td>
<td className="ri">{fInt(s.topBoost)}</td>
<td className="ri">{fRound(s.maxJumpRange)}</td>
<td className="ri">{fInt(s.maxCargo)}</td>
<td className="ri">{fInt(s.maxPassengers)}</td>
<td className="cn">{s.standard[0]}</td>
@@ -255,7 +235,6 @@ export default class ShipyardPage extends Page {
let { translate, formats, units } = language;
let hide = this.context.tooltip.bind(null, null);
let fInt = formats.int;
let fRound = formats.round;
let { shipSummaries, shipPredicate, shipPredicateIndex, compare, groupCompared } = this.state;
let sortShips = (predicate, index) =>
this._sortShips.bind(this, predicate, index);
@@ -299,7 +278,7 @@ export default class ShipyardPage extends Page {
}
if (valA == valB) {
if (a.name > b.name) {
if (a.id > b.id) {
return 1;
} else {
return -1;
@@ -335,7 +314,7 @@ export default class ShipyardPage extends Page {
onClick={() => this._toggleCompare(s.id)}
>
<td className="le">
<Link href={'/outfit/' + s.id}>{s.name} {s.beta === true ? '(Beta)' : null}</Link>
<Link href={'/outfit/' + s.id}>{s.id} {s.beta === true ? '(Beta)' : null}</Link>
</td>
</tr>
);
@@ -343,16 +322,16 @@ export default class ShipyardPage extends Page {
}
return (
<div className="page" style={{fontSize: sizeRatio + 'em'}}>
<div className="page" style={{ fontSize: sizeRatio + 'em' }}>
<div className="content-wrapper">
<div className="shipyard-table-wrapper">
<table style={{width: '12em', position: 'absolute', zIndex: 1}} className="shipyard-table">
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }} className="shipyard-table">
<thead>
<tr>
<th className="le rgt">&nbsp;</th>
</tr>
<tr className="main">
<th className="sortable le rgt" onClick={sortShips('name')}>
<th className="sortable le rgt" onClick={sortShips('id')}>
{translate('ship')}
</th>
</tr>
@@ -368,110 +347,87 @@ export default class ShipyardPage extends Page {
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table">
<thead>
<tr className="main">
<th
rowSpan={3}
className="sortable"
onClick={sortShips('manufacturer')}
>
{translate('manufacturer')}
</th>
{/* First all headers that spread out over three rows */}
{/* cost placeholder */}
<th>&nbsp;</th>
<th
rowSpan={3}
className="sortable"
onClick={sortShips('class')}
>
<th rowSpan={3} className="sortable" onClick={sortShips('class')}>
{translate('size')}
</th>
<th
rowSpan={3}
className="sortable"
onClick={sortShips('crew')}
>
<th rowSpan={3} className="sortable" onClick={sortShips('crew')}>
{translate('crew')}
</th>
<th
rowSpan={3}
className="sortable"
<th rowSpan={3} className="sortable"
onMouseEnter={termtip.bind(null, 'mass lock factor')}
onMouseLeave={hide}
onClick={sortShips('masslock')}
>
onMouseLeave={hide} onClick={sortShips('masslock')}>
{translate('MLF')}
</th>
<th
rowSpan={3}
className="sortable"
onClick={sortShips('agility')}
>
{translate('agility')}
</th>
<th
rowSpan={3}
className="sortable"
onMouseEnter={termtip.bind(null, 'hardness')}
onMouseLeave={hide}
onClick={sortShips('hardness')}
>
{translate('hrd')}
</th>
{/* hull mass placeholder */}
<th>&nbsp;</th>
<th colSpan={6}>{translate('agility')}</th>
<th colSpan={2}>{translate('travel')}</th>
<th colSpan={3}>{translate('defence')}</th>
{/* cargo placeholder */}
<th>&nbsp;</th>
{/* pax placeholder */}
<th>&nbsp;</th>
<th colSpan={4}>{translate('base')}</th>
<th colSpan={5}>{translate('max')}</th>
<th className="lft" colSpan={7} />
<th className="lft" colSpan={5} />
<th className="lft" colSpan={8} />
</tr>
<tr>
<th
className="sortable lft"
onClick={sortShips('retailCost')}
>
{/* Now all headers in a second-row */}
<th className="sortable lft" onClick={sortShips('retailCost')}>
{translate('cost')}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{translate('hull')}
</th>
<th className="sortable lft" onClick={sortShips('speed')}>
<th className="sortable lft" onClick={sortShips('agility')}>
{translate('rating')}
</th>
<th className="sortable" onClick={sortShips('speed')}>
{translate('speed')}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{translate('boost')}
</th>
<th className="sortable" onClick={sortShips('pitch')}>
{translate('pitch')}
</th>
<th className="sortable" onClick={sortShips('yaw')}>
{translate('yaw')}
</th>
<th className="sortable" onClick={sortShips('roll')}>
{translate('roll')}
</th>
<th className="sortable lft" onClick={sortShips('jumpRange')}>
{translate('jump')}
</th>
<th className="sortable" onClick={sortShips('totalRange')}>
{translate('range')}
</th>
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'hardness')}
onMouseLeave={hide} onClick={sortShips('hardness')}>
{translate('hrd')}
</th>
<th className="sortable" onClick={sortShips('baseArmour')}>
{translate('armour')}
</th>
<th
className="sortable"
onClick={sortShips('baseShieldStrength')}
>
<th className="sortable" onClick={sortShips('baseShieldStrength')}>
{translate('shields')}
</th>
<th className="sortable lft" onClick={sortShips('topSpeed')}>
{translate('speed')}
</th>
<th className="sortable" onClick={sortShips('topBoost')}>
{translate('boost')}
</th>
<th className="sortable" onClick={sortShips('maxJumpRange')}>
{translate('jump')}
</th>
<th className="sortable" onClick={sortShips('maxCargo')}>
<th className="sortable lft" onClick={sortShips('maxCargo')}>
{translate('cargo')}
</th>
<th className="sortable" onClick={sortShips('maxPassengers')} onMouseEnter={termtip.bind(null, 'passenger capacity')}
onMouseLeave={hide}>
<th className="sortable lft" onClick={sortShips('maxPassengers')}
onMouseEnter={termtip.bind(null, 'passenger capacity')} onMouseLeave={hide}>
{translate('pax')}
</th>
<th className="lft" colSpan={7}>
{translate('core module classes')}
</th>
<th
colSpan={5}
className="sortable lft"
onClick={sortShips('hpCount')}
>
<th colSpan={5} className="sortable lft" onClick={sortShips('hpCount')}>
{translate('hardpoints')}
</th>
<th
@@ -483,6 +439,7 @@ export default class ShipyardPage extends Page {
</th>
</tr>
<tr>
{/* Third row headers, i.e., units */}
<th
className="sortable lft"
onClick={sortShips('retailCost')}
@@ -492,12 +449,31 @@ export default class ShipyardPage extends Page {
<th className="sortable lft" onClick={sortShips('hullMass')}>
{units.T}
</th>
<th className="sortable lft" onClick={sortShips('speed')}>
{/* agility rating placeholder */}
<th className="lft">&nbsp;</th>
<th className="sortable" onClick={sortShips('speed')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('pitch')}>
{units['°/s']}
</th>
<th className="sortable" onClick={sortShips('yaw')}>
{units['°/s']}
</th>
<th className="sortable" onClick={sortShips('roll')}>
{units['°/s']}
</th>
<th className="sortable lft" onClick={sortShips('jumpRange')}>
{units.LY}
</th>
<th className="sortable" onClick={sortShips('totalRange')}>
{units.LY}
</th>
<th className="lft">&nbsp;</th>
{/* armour placeholder */}
<th>&nbsp;</th>
<th
className="sortable"
@@ -505,73 +481,37 @@ export default class ShipyardPage extends Page {
>
{units.MJ}
</th>
<th className="sortable lft" onClick={sortShips('topSpeed')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('topBoost')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('maxJumpRange')}>
{units.LY}
</th>
<th className="sortable" onClick={sortShips('maxCargo')}>
<th className="sortable lft" onClick={sortShips('maxCargo')}>
{units.T}
</th>
<th>&nbsp;</th>
<th
className="sortable lft"
onMouseEnter={termtip.bind(null, 'power plant')}
onMouseLeave={hide}
onClick={sortShips('standard', 0)}
>
{/* pax placeholder */}
<th className="lft">&nbsp;</th>
<th className="sortable lft" onMouseEnter={termtip.bind(null, 'power plant')}
onMouseLeave={hide} onClick={sortShips('standard', 0)}>
{'pp'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'thrusters')}
onMouseLeave={hide}
onClick={sortShips('standard', 1)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'thrusters')}
onMouseLeave={hide} onClick={sortShips('standard', 1)}>
{'th'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'frame shift drive')}
onMouseLeave={hide}
onClick={sortShips('standard', 2)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'frame shift drive')}
onMouseLeave={hide} onClick={sortShips('standard', 2)}>
{'fsd'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'life support')}
onMouseLeave={hide}
onClick={sortShips('standard', 3)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'life support')}
onMouseLeave={hide} onClick={sortShips('standard', 3)}>
{'ls'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'power distriubtor')}
onMouseLeave={hide}
onClick={sortShips('standard', 4)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'power distriubtor')}
onMouseLeave={hide} onClick={sortShips('standard', 4)}>
{'pd'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'sensors')}
onMouseLeave={hide}
onClick={sortShips('standard', 5)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'sensors')}
onMouseLeave={hide} onClick={sortShips('standard', 5)}>
{'s'}
</th>
<th
className="sortable"
onMouseEnter={termtip.bind(null, 'fuel tank')}
onMouseLeave={hide}
onClick={sortShips('standard', 6)}
>
<th className="sortable" onMouseEnter={termtip.bind(null, 'fuel tank')}
onMouseLeave={hide} onClick={sortShips('standard', 6)}>
{'ft'}
</th>
<th className="sortable lft" onClick={sortShips('hp', 1)}>

View File

@@ -0,0 +1,33 @@
import { Module } from 'ed-forge';
/**
* Sets a resistance value of a module as
* @param {Module} module Module to set the property
* @param {string} prop Property name; must end with 'resistance'
* @param {number} val Resistance value to set
*/
function setterResToEff(module, prop, val) {
module.set(
prop.replace('resistance', 'effectiveness'),
1 - val / 100,
);
}
export const SHOW = {
causticeffectiveness: {
as: 'causticresistance',
setter: setterResToEff,
},
explosiveeffectiveness: {
as: 'explosiveresistance',
setter: setterResToEff,
},
kineticeffectiveness: {
as: 'kineticresistance',
setter: setterResToEff,
},
thermiceffectiveness: {
as: 'thermicresistance',
setter: setterResToEff,
},
};

View File

@@ -1,61 +1,43 @@
import React from 'react';
import { Modifications } from 'coriolis-data/dist';
import { STATS_FORMATTING } from '../shipyard/StatsFormatting';
import { Module } from 'ed-forge';
import { getBlueprintInfo, getExperimentalInfo } from 'ed-forge/lib/data/blueprints';
import { entries, keys, uniq } from 'lodash';
/**
* Generate a tooltip with details of a blueprint's specials
* @param {Object} translate The translate object
* @param {Object} blueprint The blueprint at the required grade
* @param {string} grp The group of the module
* @param {Object} m The module to compare with
* @param {Object} language The translate object
* @param {Module} m The module to compare with
* @param {string} specialName The name of the special
* @returns {Object} The react components
*/
export function specialToolTip(translate, blueprint, grp, m, specialName) {
const effects = [];
if (!blueprint || !blueprint.features) {
return undefined;
}
if (m) {
// We also add in any benefits from specials that aren't covered above
if (m.blueprint) {
for (const feature in Modifications.modifierActions[specialName]) {
// if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature) - m.getModValue(feature, true);
if (featureDef.type === 'percentage') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature + '_specialTT'}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'}
style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
}
}
export function specialToolTip(language, m, specialName) {
const { formats, translate } = language;
return (
<div>
<table width='100%'>
<tbody>
{effects}
{entries(getExperimentalInfo(specialName).features).map(
([prop, feats]) => {
const { max, only } = feats;
if (only && !m.getItem().match(only)) {
return null;
}
const { value, unit, beneficial } = m.getModifierFormatted(prop);
// If the product of value and min/max is positive, both values
// point into the same direction, i.e. positive/negative.
const specialBeneficial = (value * max) > 0 === beneficial;
return <tr key={prop + '_specialTT'}>
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
<td>&nbsp;</td>
<td className={specialBeneficial ? 'secondary' : 'warning'}
style={{ textAlign: 'right' }}>{formats.round(max * 100)}{unit}</td>
<td>&nbsp;</td>
</tr>;
}
)}
</tbody>
</table>
</div>
@@ -63,154 +45,23 @@ export function specialToolTip(translate, blueprint, grp, m, specialName) {
}
/**
* Generate a tooltip with details of a blueprint's effects
* @param {Object} translate The translate object
* @param {Object} blueprint The blueprint at the required grade
* @param {Array} engineers The engineers supplying this blueprint
* @param {string} grp The group of the module
* @param {Object} m The module to compare with
* Generate a tooltip with details and preview of a blueprint's effects
* @param {Object} language The language object
* @param {Module} m The module to compare with
* @param {string} previewBP Blueprint to preview
* @param {number} previewGrade Grade to preview
* @returns {Object} The react components
*/
export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
const effects = [];
if (!blueprint || !blueprint.features) {
return undefined;
}
for (const feature in blueprint.features) {
const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]);
const featureDef = Modifications.modifications[feature];
if (!featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let lowerBound = blueprint.features[feature][0];
let upperBound = blueprint.features[feature][1];
if (featureDef.type === 'percentage') {
lowerBound = Math.round(lowerBound * 1000) / 10;
upperBound = Math.round(upperBound * 1000) / 10;
}
const lowerIsBeneficial = isValueBeneficial(feature, lowerBound);
const upperIsBeneficial = isValueBeneficial(feature, upperBound);
if (m) {
// We have a module - add in the current value
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
} else {
// We do not have a module, no value
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td className={lowerBound === 0 ? '' : lowerIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{lowerBound}{symbol}</td>
<td className={upperBound === 0 ? '' : upperIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{upperBound}{symbol}</td>
</tr>
);
}
}
}
if (m) {
// Because we have a module add in any benefits that aren't part of the primary blueprint
for (const feature in m.mods) {
if (!blueprint.features[feature]) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
export function blueprintTooltip(language, m, previewBP, previewGrade) {
const { translate, formats } = language;
const blueprint = previewBP || m.getBlueprint();
const grade = previewGrade || m.getBlueprintGrade();
if (!blueprint) {
return null;
}
// We also add in any benefits from specials that aren't covered above
if (m.blueprint && m.blueprint.special) {
for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) {
if (!blueprint.features[feature] && !m.mods.feature) {
const featureDef = Modifications.modifications[feature];
if (featureDef && !featureDef.hidden) {
let symbol = '';
if (feature === 'jitter') {
symbol = '°';
} else if (featureDef.type === 'percentage') {
symbol = '%';
}
let current = m.getModValue(feature);
if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') {
current = Math.round(current / 10) / 10;
} else if (featureDef.type === 'numeric') {
current /= 100;
}
const currentIsBeneficial = isValueBeneficial(feature, current);
effects.push(
<tr key={feature}>
<td style={{ textAlign: 'left' }}>{translate(feature, grp)}</td>
<td>&nbsp;</td>
<td className={current === 0 ? '' : currentIsBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>{current}{symbol}</td>
<td>&nbsp;</td>
</tr>
);
}
}
}
}
}
let components;
if (!m) {
components = [];
for (const component in blueprint.components) {
components.push(
<tr key={component}>
<td style={{ textAlign: 'left' }}>{translate(component)}</td>
<td style={{ textAlign: 'right' }}>{blueprint.components[component]}</td>
</tr>
);
}
}
let engineersList;
if (engineers) {
engineersList = [];
for (const engineer of engineers) {
engineersList.push(
<tr key={engineer}>
<td style={{ textAlign: 'left' }}>{engineer}</td>
</tr>
);
}
}
const bpFeatures = getBlueprintInfo(blueprint).features[grade];
const features = uniq(m.getModifiedProperties().concat(keys(bpFeatures)));
return (
<div>
@@ -219,87 +70,45 @@ export function blueprintTooltip(translate, blueprint, engineers, grp, m) {
<tr>
<td>{translate('feature')}</td>
<td>{translate('worst')}</td>
{m ? <td>{translate('current')}</td> : null }
<td>{translate('current')}</td>
<td>{translate('best')}</td>
</tr>
</thead>
<tbody>
{effects}
{features.map((prop) => {
const { min, max, only } = bpFeatures[prop] || {};
// Skip this property if it doesn't apply to this module
if (only && !m.getItem().match(only)) {
return null;
}
const { value, unit, beneficial } = m.getModifierFormatted(prop);
if (!bpFeatures[prop] && !value) {
// Can happen for exported synthetics
return null;
}
// If the product of value and min/max is positive, both values
// point into the same direction, i.e. positive/negative.
const minBeneficial = (value * min) > 0 === beneficial;
const maxBeneficial = (value * max) > 0 === beneficial;
return (<tr key={prop}>
<td style={{ textAlign: 'left' }}>{translate(prop)}</td>
<td className={!min ? '' : minBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{!isNaN(min) && formats.round(min * 100)}{!isNaN(min) && unit}
</td>
<td className={!value ? '' : beneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{formats.round(value || 0)}{unit}
</td>
<td className={!max ? '' : maxBeneficial ? 'secondary' : 'warning'} style={{ textAlign: 'right' }}>
{!isNaN(max) && formats.round(max * 100)}{!isNaN(max) && unit}
</td>
</tr>);
})}
</tbody>
</table>
{ components ? <table width='100%'>
<thead>
<tr>
<td>{translate('component')}</td>
<td>{translate('amount')}</td>
</tr>
</thead>
<tbody>
{components}
</tbody>
</table> : null }
{ engineersList ? <table width='100%'>
<thead>
<tr>
<td>{translate('engineers')}</td>
</tr>
</thead>
<tbody>
{engineersList}
</tbody>
</table> : null }
</div>
);
}
/**
* Is this blueprint feature beneficial?
* @param {string} feature The name of the feature
* @param {array} values The value of the feature
* @returns {boolean} True if this feature is beneficial
*/
export function isBeneficial(feature, values) {
const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0));
if (Modifications.modifications[feature].higherbetter) {
return !fact;
} else {
return fact;
}
}
/**
* Is this feature value beneficial?
* @param {string} feature The name of the feature
* @param {number} value The value of the feature
* @returns {boolean} True if this value is beneficial
*/
export function isValueBeneficial(feature, value) {
if (Modifications.modifications[feature].higherbetter) {
return value > 0;
} else {
return value < 0;
}
}
/**
* Is the change as shown beneficial?
* @param {string} feature The name of the feature
* @param {number} value The value of the feature as percentage change
* @returns True if the value is beneficial
*/
export function isChangeValueBeneficial(feature, value) {
let changeHigherBetter = STATS_FORMATTING[feature].higherbetter;
if (changeHigherBetter === undefined) {
return isValueBeneficial(feature, value);
}
if (changeHigherBetter) {
return value > 0;
} else {
return value < 0;
}
}
/**
* Get a blueprint with a given name and an optional module
* @param {string} name The name of the blueprint
@@ -316,120 +125,3 @@ export function getBlueprint(name, module) {
const blueprint = JSON.parse(JSON.stringify(found));
return blueprint;
}
/**
* Provide 'percent' primary modifications
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
* @param {Number} percent The percent to set values to of full.
*/
export function setPercent(ship, m, percent) {
ship.clearModifications(m);
// Pick given value as multiplier
const mult = percent / 100;
const features = m.blueprint.grades[m.blueprint.grade].features;
for (const featureName in features) {
let value;
if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
} else {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
}
} else {
// Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * mult);
} else {
value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * mult);
}
}
_setValue(ship, m, featureName, value);
}
}
/**
* Provide 'random' primary modifications
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
*/
export function setRandom(ship, m) {
// Pick a single value for our randomness
setPercent(ship, m, Math.random() * 100);
}
/**
* Set a modification feature value
* @param {Object} ship The ship for which to perform the modifications
* @param {Object} m The module for which to perform the modifications
* @param {string} featureName The feature being set
* @param {number} value The value being set for the feature
*/
function _setValue(ship, m, featureName, value) {
if (Modifications.modifications[featureName].type == 'percentage') {
ship.setModification(m, featureName, value * 10000);
} else if (Modifications.modifications[featureName].type == 'numeric') {
ship.setModification(m, featureName, value * 100);
} else {
ship.setModification(m, featureName, value);
}
}
/**
* Provide 'percent' primary query
* @param {Object} m The module for which to perform the query
* @returns {Number} percent The percentage indicator of current applied values.
*/
export function getPercent(m) {
let result = null;
const features = m.blueprint.grades[m.blueprint.grade].features;
for (const featureName in features) {
if (features[featureName][0] === features[featureName][1]) {
continue;
}
let value = _getValue(m, featureName);
let mult;
if (Modifications.modifications[featureName].higherbetter) {
// Higher is better, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
} else {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
}
} else {
// Higher is worse, but is this making it better or worse?
if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) {
mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100);
} else {
mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100);
}
}
if (result && result != mult) {
return null;
} else if (result != mult) {
result = mult;
}
}
return result;
}
/**
* Query a feature value
* @param {Object} m The module for which to perform the query
* @param {string} featureName The feature being queried
* @returns {number} The value of the modification as a %
*/
function _getValue(m, featureName) {
if (Modifications.modifications[featureName].type == 'percentage') {
return m.getModValue(featureName, true) / 10000;
} else if (Modifications.modifications[featureName].type == 'numeric') {
return m.getModValue(featureName, true) / 100;
} else {
return m.getModValue(featureName, true);
}
}

View File

@@ -1,3 +1,4 @@
import { Module } from 'ed-forge';
/**
* Wraps the callback/context menu handler such that the default
@@ -83,3 +84,18 @@ export function isEmpty(obj) {
}
return true;
};
/**
* Fetches a property from either a Module or a moduleInfo object
* @param {Object} m Either a Module or a moduleInfo object
* @param {string} property Property name
* @returns {number} Property value
*/
export function moduleGet(m, property) {
if (m instanceof Module) {
return m.get(property);
} else {
// Assume its a moduleInfo object
return m.props[property];
}
}

View File

@@ -170,11 +170,8 @@ select {
list-style: none;
}
&.hardpoint {
.c {
.hardpoint {
width: 4.5em;
padding: 0.1em 0.2em;
}
}
}