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 { Insurance } from '../shipyard/Constants';
import { slotName, slotComparator } from '../utils/SlotFunctions';
import TranslatedComponent from './TranslatedComponent';
import { ShoppingIcon } from '../components/SvgIcons';
/**
* Cost Section
*/
export default class CostSection extends TranslatedComponent {
static propTypes = {
ship: PropTypes.object.isRequired,
code: PropTypes.string.isRequired,
buildName: PropTypes.string
};
/**
* Constructor
* @param {Object} props React Component properties
*/
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);
this.state = {
retrofitShip,
retrofitName,
shipDiscount,
moduleDiscount,
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
};
}
/**
* 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
} else {
retrofitShip.buildWith(data.defaults); // Populate with default components
}
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;
}
/**
* Show selected tab
* @param {string} tab Tab name
*/
_showTab(tab) {
Persist.setCostTab(tab);
this.setState({ tab });
}
/**
* Update prices on discount change
*/
_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 });
}
/**
* Update insurance on change
* @param {string} insuranceName Insurance level name
*/
_onInsuranceChanged(insuranceName) {
this.setState({ insurance: Insurance[insuranceName] });
}
/**
* Repopulate modules on retrofit ship from existing build
* @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 });
}
/**
* On builds changed check to see if the retrofit ship needs
* to be updated
*/
_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) });
}
/**
* Toggle item cost inclusion in overall total
* @param {Object} item Cost item
*/
_toggleCost(item) {
this.props.ship.setCostIncluded(item, !item.incCost);
this.forceUpdate();
}
/**
* Toggle item cost inclusion in retrofit total
* @param {Object} item Cost item
*/
_toggleRetrofitCost(item) {
let retrofitTotal = this.state.retrofitTotal;
item.retroItem.incCost = !item.retroItem.incCost;
retrofitTotal += item.netCost * (item.retroItem.incCost ? 1 : -1);
this.setState({ retrofitTotal });
}
/**
* Set cost list sort predicate
* @param {string} predicate sort predicate
*/
_sortCostBy(predicate) {
let { costPredicate, costDesc } = this.state;
if (costPredicate == predicate) {
costDesc = !costDesc;
}
this.setState({ costPredicate: predicate, costDesc });
}
/**
* Sort cost list
* @param {Ship} ship Ship instance
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortCost(ship, predicate, desc) {
let costList = ship.costList;
let translate = this.context.language.translate;
if (predicate == 'm') {
costList.sort(slotComparator(translate, null, desc));
} else {
costList.sort(slotComparator(translate, (a, b) => (a.m.cost || 0) - (b.m.cost || 0), desc));
}
}
/**
* Set ammo list sort predicate
* @param {string} predicate sort predicate
*/
_sortAmmoBy(predicate) {
let { ammoPredicate, ammoDesc } = this.state;
if (ammoPredicate == predicate) {
ammoDesc = !ammoDesc;
}
this.setState({ ammoPredicate: predicate, ammoDesc });
}
/**
* Sort ammo cost list
* @param {Array} ammoCosts Ammo cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortAmmo(ammoCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'm') {
ammoCosts.sort(slotComparator(translate, null, desc));
} else {
ammoCosts.sort(slotComparator(translate, (a, b) => a[predicate] - b[predicate], desc));
}
}
/**
* Set retrofit list sort predicate
* @param {string} predicate sort predicate
*/
_sortRetrofitBy(predicate) {
let { retroPredicate, retroDesc } = this.state;
if (retroPredicate == predicate) {
retroDesc = !retroDesc;
}
this.setState({ retroPredicate: predicate, retroDesc });
}
/**
* Sort retrofit cost list
* @param {Array} retrofitCosts Retrofit cost list
* @param {string} predicate Sort predicate
* @param {Boolean} desc Sort descending
*/
_sortRetrofit(retrofitCosts, predicate, desc) {
let translate = this.context.language.translate;
if (predicate == 'cr') {
retrofitCosts.sort((a, b) => a.netCost - b.netCost);
} else {
retrofitCosts.sort((a , b) => (a[predicate] ? translate(a[predicate]).toLowerCase() : '').localeCompare(b[predicate] ? translate(b[predicate]).toLowerCase() : ''));
}
if (!desc) {
retrofitCosts.reverse();
}
}
/**
* Render the cost tab
* @return {React.Component} Tab contents
*/
_costsTab() {
let { ship } = this.props;
let { shipDiscount, moduleDiscount, insurance } = 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(
{item.m.class + item.m.rating}
{slotName(translate, item)}
{formats.int(item.discountedCost)}{units.CR}
);
}
}
return
{translate('module')}
{shipDiscount ? {`[${translate('ship')} -${formats.pct(shipDiscount)}]`} : null}
{moduleDiscount ? {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`} : null}
{translate('credits')}
{rows}
{translate('total')}
{formats.int(ship.totalCost)}{units.CR}
{translate('insurance')}
{formats.int(ship.totalCost * insurance)}{units.CR}
;
}
/**
* Open up a window for EDDB with a shopping list of our retrofit components
*/
_eddbShoppingList() {
const { retrofitCosts } = 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);
// Open up the relevant URL
window.open('https://eddb.io/station?m=' + modIds.join(','));
}
/**
* Render the retofit tab
* @return {React.Component} Tab contents
*/
_retrofitTab() {
let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state;
const { termtip, tooltip } = this.context;
let { translate, formats, units } = this.context.language;
let int = formats.int;
let rows = [], options = [{translate('Stock')} ];
for (let opt of this.state.buildOptions) {
options.push({opt} );
}
if (retrofitCosts.length) {
for (let i = 0, l = retrofitCosts.length; i < l; i++) {
let item = retrofitCosts[i];
rows.push(
{item.sellClassRating}
{translate(item.sellName)}
{item.buyClassRating}
{translate(item.buyName)}
0 ? 'warning' : 'secondary-disabled' : 'disabled')}>{int(item.netCost)}{units.CR}
);
}
} else {
rows = {translate('PHRASE_NO_RETROCH')} ;
}
return
{translate('sell')}
{translate('buy')}
{translate('net cost')}
{moduleDiscount ? {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`} : null}
{rows}
{translate('cost')}
0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}>
{int(retrofitTotal)}{units.CR}
{translate('retrofit from')}
▾
{options}
;
}
/**
* Update retrofit costs
* @param {Ship} ship Ship instance
* @param {Ship} retrofitShip Retrofit Ship instance
*/
_updateRetrofit(ship, retrofitShip) {
let retrofitCosts = [];
let retrofitTotal = 0, i, l, item;
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;
}
}
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;
}
}
}
}
this.setState({ retrofitCosts, retrofitTotal });
this._sortRetrofit(retrofitCosts, this.state.retroPredicate, this.state.retroDesc);
}
/**
* Render the ammo tab
* @return {React.Component} Tab contents
*/
_ammoTab() {
let { ammoTotal, ammoCosts } = this.state;
let { translate, formats, units } = this.context.language;
let int = formats.int;
let rows = [];
for (let i = 0, l = ammoCosts.length; i < l; i++) {
let item = ammoCosts[i];
rows.push(
{item.m.class + item.m.rating}
{slotName(translate, item)}
{int(item.max)}
{int(item.cost)}{units.CR}
{int(item.total)}{units.CR}
);
}
return
{translate('module')}
{translate('qty')}
{translate('unit cost')}
{translate('subtotal')}
{rows}
{translate('total')}
{int(ammoTotal)}{units.CR}
;
}
/**
* 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
*/
componentWillMount() {
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);
}
}
}
/**
* Remove listeners
*/
componentWillUnmount() {
this.listeners.forEach(l => l.remove());
}
/**
* Render the Cost section
* @return {React.Component} Contents
*/
render() {
let tab = this.state.tab;
let translate = this.context.language.translate;
let tabSection;
switch (tab) {
case 'ammo': tabSection = this._ammoTab(); break;
case 'retrofit': tabSection = this._retrofitTab(); break;
default:
tab = 'costs';
tabSection = this._costsTab();
}
return (
{translate('costs')}
{translate('retrofit costs')}
{translate('reload costs')}
{tabSection}
);
}
}