Compare commits

...

60 Commits

Author SHA1 Message Date
Felix Linker
2eed1bc85b Add time to deplete armour/shields 2021-05-15 20:21:00 +02:00
Felix Linker
3469af10b6 Update ShipPicker 2021-05-15 15:23:36 +02:00
Felix Linker
629ba35bc5 Update engagement range and slider 2021-05-15 14:58:10 +02:00
Felix Linker
e453ff73b7 Migrate changes to slot-representation 2021-05-14 10:22:28 +02:00
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
59 changed files with 28247 additions and 5310 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,232 +48,171 @@ 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] = [];
}
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(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);
}
}
}
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>);
}
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}`,
};
}),
);
}
}
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;
}
// 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);
};
eventHandlers = {
onMouseEnter: this._over.bind(this, showDiff),
onTouchStart: this._touchStart.bind(this, showDiff),
onTouchEnd: this._touchEnd.bind(this, select),
onMouseLeave: this._hideDiff,
onClick: select,
};
}
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>
);
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]);
}
elems.push(...newTail);
}
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)
};
} 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),
onTouchStart: this._touchStart.bind(this, showDiff),
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>
);
itemsOnThisRow++;
prevClass = m.class;
prevRating = m.rating;
prevName = m.name;
}
return <ul key={'modules' + grp}>{elems}</ul>;
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();
}
}
@@ -570,10 +320,10 @@ export default class AvailableModulesMenu extends TranslatedComponent {
render() {
return (
<div ref={node => this.node = node}
className={cn('select', this.props.className)}
onScroll={this._hideDiff}
onClick={(e) => e.stopPropagation()}
onContextMenu={stopCtxPropagation}
className={cn('select', this.props.className)}
onScroll={this._hideDiff}
onClick={(e) => e.stopPropagation()}
onContextMenu={stopCtxPropagation}
>
{this._showSearch()}
{this.state.list}

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

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,32 +51,24 @@ 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}
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}
enabled={h.enabled ? true : false}
/>);
}
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)}
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,12 +56,11 @@ function selectAll(e) {
* Coriolis App Header section / menus
*/
export default class Header extends TranslatedComponent {
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
/**
* Constructor
* @param {Object} props React Component properties
* @param {Object} context React Component context
*/
constructor(props, context) {
super(props);
this.shipOrder = Object.keys(Ships).sort();
@@ -509,7 +508,7 @@ export default class Header extends TranslatedComponent {
<td style={{ width: 20 }}><span style={{ fontSize: 30 }}>A</span></td>
</tr>
<tr>
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
<td colSpan='3' style={{ textAlign: 'center', cursor: 'pointer' }} className='primary-disabled cap' onClick={this._resetTextSize.bind(this)}>{translate('reset')}</td>
</tr>
</tbody>
</table>
@@ -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';
}
@@ -542,7 +540,7 @@ export default class ModalImport extends TranslatedComponent {
{comparisonRows}
</tbody>
</table>
);
);
}
if(this.state.canEdit) {

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
};
@@ -58,14 +57,14 @@ export default class ModalOrbis extends TranslatedComponent {
credentials: 'include',
mode: 'cors'
})
.then(data => data.json())
.then(res => {
this.setState({ authenticatedStatus: res.status || res.error });
})
.catch(err => {
console.error(err);
this.setState({ authenticatedStatus: err.message });
});
.then(data => data.json())
.then(res => {
this.setState({ authenticatedStatus: res.status || res.error });
})
.catch(err => {
console.error(err);
this.setState({ authenticatedStatus: err.message });
});
}
/**
@@ -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'}>
<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' }}/>
}
<span className={'unit-container'}>
{units[m.getStoredUnitFor(name)]}
</span>
</span>
</td>
<td style={{ textAlign: 'center' }} className={
modValue ?
isChangeValueBeneficial(name, modValue) ? 'secondary' : 'warning' :
''
}>
{formats.f2(modValue / 100) || 0}{isOverwrite ? '' : '%'}
</td>
</tr>
</tbody>
</table>
</div>
<tr>
<td>
<span>
<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();
}
}}
onValueChange={(inputValue) => {
if (inputValue.length <= 15) {
this.setState({ inputValue });
}
}} />
</span>
</td>
<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>
);
}
}

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,327 +34,177 @@ 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 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;
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>;
});
return [
<div key={'div' + blueprint} className={'select-group cap'}>
{translate(blueprint)}
</div>,
<ul key={'ul' + blueprint}>{blueprintGrades}</ul>
];
});
return flatMap(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;
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;
}
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
* @param {Object} props React component properties
* @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 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;
}
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>);
}
}
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>
);
}
return specials;
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>
);
}
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 [
<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]);
}
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();
}
return () => {
const { m } = this.props;
m.setExperimental(special);
this.setState({ specialMenuOpened: false });
};
}
/**
@@ -370,32 +213,12 @@ 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();
return;
} else if (this.modItems[this.selectedSpecialId]) {
this.modItems[this.selectedSpecialId].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();
if (this.selectedModRef) {
this.selectedModRef.focus();
return;
} else if (this.selectedSpecialRef) {
this.selectedSpecialRef.focus();
return;
}
}
@@ -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');
}
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}
>
{translate(appliedBlueprint)} {translate('grade')} {appliedGrade}
</div>
);
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}
>
{ 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 ?
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 }
<tr>
<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
* @returns {Object} Data object
* @param {function} translate Translation function
* @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,234 @@ 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 {
drained, sustained, rangeMultiplier, hardnessMultiplier, timeToDrain
} = ship.getMetrics(DAMAGE_METRICS);
const portions = {
Absolute: sustained.types.abs,
Explosive: sustained.types.expl,
Kinetic: sustained.types.kin,
Thermic: sustained.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,
};
const weapons = sortBy(ship.getHardpoints(), (m) => m.get('distributordraw'));
let rows = weapons.map((weapon) => {
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 bp = weapon.getBlueprint();
const grade = weapon.getBlueprintGrade();
const exp = weapon.getExperimental();
let bpTitle = `${translate(bp)} ${translate('grade')} ${grade}`;
if (exp) {
bpTitle += `, ${translate(exp)}`;
}
return {
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 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 { 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(sustained.dps * p)),
);
const sdpsPie = objToPie(
translate,
mapValues(portions, (p) => Math.round(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) => sustained.dps * oppShield.absolute.bySys *
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) => sustained.dps * hardnessMultiplier * 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 drainedPortions = {
Absolute: drained.types.abs,
Explosive: drained.types.expl,
Kinetic: drained.types.kin,
Thermic: drained.types.therm,
};
// How much damage do we deal, before the capacitor is empty?
const armourLeft = oppArmour.armour - (timeToDrain * armourSdps);
// If we can't kill the enemy on one capacitor, factor in drained damage
let timeToDepleteArmour;
if (armourLeft > 0) {
const effectiveDrainedDps = sum(values(mergeWith(
clone(drainedPortions),
armourMults,
(objV, srcV) => objV * srcV,
))) * drained.dps * rangeMultiplier *
hardnessMultiplier;
timeToDepleteArmour = effectiveDrainedDps === 0 ? Infinity :
timeToDrain + (armourLeft / effectiveDrainedDps);
} else {
timeToDepleteArmour = oppArmour.armour / armourSdps;
}
// How much damage do we deal, before the capacitor is empty?
const shieldsLeft = oppShield.withSCBs - (timeToDrain * shieldsSdps);
// If we can't kill the enemy on one capacitor, factor in drained damage
let timeToDepleteShields;
if (shieldsLeft > 0) {
const effectiveDrainedDps = sum(values(mergeWith(
clone(drainedPortions),
shieldMults,
(objV, srcV) => objV * srcV,
))) * drained.dps * rangeMultiplier;
timeToDepleteShields = effectiveDrainedDps === 0 ? Infinity :
timeToDrain + (shieldsLeft / effectiveDrainedDps);
} else {
timeToDepleteShields = oppShield.withSCBs / shieldsSdps;
}
return (
<span id='offence'>
@@ -269,28 +290,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(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 +367,51 @@ 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/>
{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/>
{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 className='group full' style={{ minHeight: '1000px' }}>
<table className='tabs'>
<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>
</tr>
</thead>
</table>
{tabSection}
<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>
</tr>
</thead>
</table>
{/* 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
* @return {string} CSS Class name
* @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();
}
}
/**
* Toggle slot active/inactive
* @param {Object} slot Slot model
*/
_toggleEnabled(slot) {
this.props.ship.setSlotEnabled(slot, !slot.enabled);
this.props.onChange();
_prioCb(m, delta) {
return () => {
const prio = m.getPowerPriority();
const newPrio = Math.max(0, prio + delta);
if (0 <= newPrio) {
m.setPowerPriority(newPrio);
}
};
}
/**
@@ -110,36 +134,35 @@ 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 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>;
} else {
retractedElem = <td className='ptr disabled upp' colSpan='2' onClick={toggleEnabled}>{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>
<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>
</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>);
let modules = this._sortAndFilter(ship.getModules());
for (let m of modules) {
let retractedElem = null, deployedElem = null;
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={flipEnabled}>{translate('disabled')}</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._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>
{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

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

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>
@@ -153,21 +223,21 @@ export default class ShipSummaryTable extends TranslatedComponent {
</tr>
</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></td>
<tr>
<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></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>
</tr>
<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>{formats.time(sgMetrics.recover) || translate('Never')}</td>
<td>{formats.time(sgMetrics.recharge) || translate('Never')}</td>
</tr>
</tbody>
<thead>
<tr>
@@ -179,7 +249,7 @@ export default class ShipSummaryTable extends TranslatedComponent {
<th rowSpan={2} onMouseEnter={termtip.bind(null, 'TT_MODULE_PROTECTION_INTERNAL', { cap: 0 })} onMouseLeave={hide} className='lft'>{translate('internal protection')}</th>
</tr>
<tr>
<th>{`${translate('explosive')}`}</th>
<th>{`${translate('explosive')}`}</th>
<th>{`${translate('kinetic')}`}</th>
<th>{`${translate('thermal')}`}</th>
<th>{`${translate('caustic')}`}</th>
@@ -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

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

View File

@@ -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 { TYPES } 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() {
return null;
_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.isOnSlot(TYPES.MILITARY) ? '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 m.isOnSlot(TYPES.HARDPOINT):
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);
}
/** 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);
const { m } = this.props;
m.reset();
if (this.props.currentMenu === m.getSlot()) {
this.context.closeMenu();
} else {
this.forceUpdate();
}
}
/**
* 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>
{menu}
<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>
{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

@@ -247,7 +247,7 @@ export class OrbisIcon extends SvgIcon {
<path d="m155.34 679.12 173.25-190.21-15.626-13.721-170.9 190.4zm31.01 31.714 202.41-169.1-16.418-14.417-198.76 170.43z"/>
<path d="m702.66 178.87-173.25 190.21 15.625 13.721 170.9-190.4zm-31.01-31.714-202.41 169.1 16.418 14.417 198.76-170.43z" />
<rect transform="matrix(-.7071 -.7071 .7071 -.7071 429.34 1036.2)" x="387.09" y="420.77" width="84.379" height="16.859" />
</g>);
</g>);
}
}
@@ -741,9 +741,9 @@ export class Modified extends SvgIcon {
*/
svg() {
return <g>
<path d="M100,5L18,52.5L18,147.5L100,195L182,147.5L182,52.5L100,5Z"/>
<path d="M100,70L74,85L74,115L100,130L126,115L126,85L100,70Z"/>
</g>;
<path d="M100,5L18,52.5L18,147.5L100,195L182,147.5L182,52.5L100,5Z"/>
<path d="M100,70L74,85L74,115L100,130L126,115L126,85L100,70Z"/>
</g>;
}
}

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,31 +52,24 @@ 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)}
onChange={this.props.onChange}
selected={currentMenu == h}
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}
enabled={h.enabled ? true : false}
/>);
}
for (let h of ship.getUtilities(undefined, true)) {
slots.push(<Slot
key={h.object.Slot}
maxClass={h.getSize()}
onChange={this.props.onChange}
currentMenu={currentMenu}
drag={this._drag.bind(this, h)}
dragOver={this._dragOverSlot.bind(this, h)}
drop={this._drop}
dropClass={this._dropClass(h, originSlot, targetSlot)}
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
@@ -46,7 +45,7 @@ export default class ErrorDetails extends React.Component {
<h1>Jameson, we have a problem..</h1>
<h1><small>{error.message}</small></h1>
<br/>
{importerror ? <div>If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.</div> : null }
{importerror ? <div>If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.</div> : null }
<br/>
<div>Please note that this site uses Google Analytics to track performance and usage. If you are blocking cookies, for example using Ghostery, please disable blocking for this site and try again.</div>
<br/>

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,288 +322,249 @@ 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">
<thead>
<tr>
<th className="le rgt">&nbsp;</th>
</tr>
<tr className="main">
<th className="sortable le rgt" onClick={sortShips('name')}>
{translate('ship')}
</th>
</tr>
<tr>
<th className="le rgt invisible">{units['m/s']}</th>
</tr>
</thead>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}>
{shipRows}
</tbody>
</table>
<div style={{ overflowX: 'auto', maxWidth: '100%' }}>
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table">
<div className="shipyard-table-wrapper">
<table style={{ width: '12em', position: 'absolute', zIndex: 1 }} className="shipyard-table">
<thead>
<tr>
<th className="le rgt">&nbsp;</th>
</tr>
<tr className="main">
<th
rowSpan={3}
className="sortable"
onClick={sortShips('manufacturer')}
>
{translate('manufacturer')}
</th>
<th>&nbsp;</th>
<th
rowSpan={3}
className="sortable"
onClick={sortShips('class')}
>
{translate('size')}
</th>
<th
rowSpan={3}
className="sortable"
onClick={sortShips('crew')}
>
{translate('crew')}
</th>
<th
rowSpan={3}
className="sortable"
onMouseEnter={termtip.bind(null, 'mass lock factor')}
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>
<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')}
>
{translate('cost')}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{translate('hull')}
</th>
<th className="sortable lft" onClick={sortShips('speed')}>
{translate('speed')}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{translate('boost')}
</th>
<th className="sortable" onClick={sortShips('baseArmour')}>
{translate('armour')}
</th>
<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')}>
{translate('cargo')}
</th>
<th className="sortable" 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')}
>
{translate('hardpoints')}
</th>
<th
colSpan={8}
className="sortable lft"
onClick={sortShips('intCount')}
>
{translate('internal compartments')}
<th className="sortable le rgt" onClick={sortShips('id')}>
{translate('ship')}
</th>
</tr>
<tr>
<th
className="sortable lft"
onClick={sortShips('retailCost')}
>
{units.CR}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{units.T}
</th>
<th className="sortable lft" onClick={sortShips('speed')}>
{units['m/s']}
</th>
<th className="sortable" onClick={sortShips('boost')}>
{units['m/s']}
</th>
<th>&nbsp;</th>
<th
className="sortable"
onClick={sortShips('baseShieldStrength')}
>
{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')}>
{units.T}
</th>
<th>&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'}
</th>
<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)}
>
{'ls'}
</th>
<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)}
>
{'s'}
</th>
<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)}>
{translate('S')}
</th>
<th className="sortable" onClick={sortShips('hp', 2)}>
{translate('M')}
</th>
<th className="sortable" onClick={sortShips('hp', 3)}>
{translate('L')}
</th>
<th className="sortable" onClick={sortShips('hp', 4)}>
{translate('H')}
</th>
<th className="sortable" onClick={sortShips('hp', 0)}>
{translate('U')}
</th>
<th className="sortable lft" onClick={sortShips('int', 0)}>
1
</th>
<th className="sortable" onClick={sortShips('int', 1)}>
2
</th>
<th className="sortable" onClick={sortShips('int', 2)}>
3
</th>
<th className="sortable" onClick={sortShips('int', 3)}>
4
</th>
<th className="sortable" onClick={sortShips('int', 4)}>
5
</th>
<th className="sortable" onClick={sortShips('int', 5)}>
6
</th>
<th className="sortable" onClick={sortShips('int', 6)}>
7
</th>
<th className="sortable" onClick={sortShips('int', 7)}>
8
</th>
<th className="le rgt invisible">{units['m/s']}</th>
</tr>
</thead>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}>
{detailRows}
{shipRows}
</tbody>
</table>
<div style={{ overflowX: 'auto', maxWidth: '100%' }}>
<table style={{ marginLeft: 'calc(12em - 1px)', zIndex: 0 }} className="shipyard-table">
<thead>
<tr className="main">
{/* First all headers that spread out over three rows */}
{/* cost placeholder */}
<th>&nbsp;</th>
<th rowSpan={3} className="sortable" onClick={sortShips('class')}>
{translate('size')}
</th>
<th rowSpan={3} className="sortable" onClick={sortShips('crew')}>
{translate('crew')}
</th>
<th rowSpan={3} className="sortable"
onMouseEnter={termtip.bind(null, 'mass lock factor')}
onMouseLeave={hide} onClick={sortShips('masslock')}>
{translate('MLF')}
</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 className="lft" colSpan={7} />
<th className="lft" colSpan={5} />
<th className="lft" colSpan={8} />
</tr>
<tr>
{/* 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('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')}>
{translate('shields')}
</th>
<th className="sortable lft" onClick={sortShips('maxCargo')}>
{translate('cargo')}
</th>
<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')}>
{translate('hardpoints')}
</th>
<th
colSpan={8}
className="sortable lft"
onClick={sortShips('intCount')}
>
{translate('internal compartments')}
</th>
</tr>
<tr>
{/* Third row headers, i.e., units */}
<th
className="sortable lft"
onClick={sortShips('retailCost')}
>
{units.CR}
</th>
<th className="sortable lft" onClick={sortShips('hullMass')}>
{units.T}
</th>
{/* 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"
onClick={sortShips('baseShieldStrength')}
>
{units.MJ}
</th>
<th className="sortable lft" onClick={sortShips('maxCargo')}>
{units.T}
</th>
{/* 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'}
</th>
<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)}>
{'ls'}
</th>
<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)}>
{'s'}
</th>
<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)}>
{translate('S')}
</th>
<th className="sortable" onClick={sortShips('hp', 2)}>
{translate('M')}
</th>
<th className="sortable" onClick={sortShips('hp', 3)}>
{translate('L')}
</th>
<th className="sortable" onClick={sortShips('hp', 4)}>
{translate('H')}
</th>
<th className="sortable" onClick={sortShips('hp', 0)}>
{translate('U')}
</th>
<th className="sortable lft" onClick={sortShips('int', 0)}>
1
</th>
<th className="sortable" onClick={sortShips('int', 1)}>
2
</th>
<th className="sortable" onClick={sortShips('int', 2)}>
3
</th>
<th className="sortable" onClick={sortShips('int', 3)}>
4
</th>
<th className="sortable" onClick={sortShips('int', 4)}>
5
</th>
<th className="sortable" onClick={sortShips('int', 5)}>
6
</th>
<th className="sortable" onClick={sortShips('int', 6)}>
7
</th>
<th className="sortable" onClick={sortShips('int', 7)}>
8
</th>
</tr>
</thead>
<tbody onMouseLeave={this._highlightShip.bind(this, null)}>
{detailRows}
</tbody>
</table>
</div>
</div>
<div className="table-tools" >
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>Group highlighted ships</label>
</div>
</div>
<div className="table-tools" >
<label><input type="checkbox" checked={this.state.groupCompared} onClick={() => this._toggleGroupCompared()}/>Group highlighted ships</label>
</div>
</div>
</div>
);

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
* @returns {Object} The react components
* 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>
);
}
}
}
// 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>
);
}
}
}
}
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;
}
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 {
width: 4.5em;
padding: 0.1em 0.2em;
}
.hardpoint {
width: 4.5em;
padding: 0.1em 0.2em;
}
}