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
{rows}
{translate('module')} {shipDiscount ? {`[${translate('ship')} -${formats.pct(shipDiscount)}]`} : null} {moduleDiscount ? {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`} : null} {translate('credits')}
{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 = []; for (let opt of this.state.buildOptions) { options.push(); } 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
{rows}
{translate('sell')} {translate('buy')} {translate('net cost')} {moduleDiscount ? {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`} : null}
{translate('cost')} 0 ? 'warning' : 'secondary-disabled')} style={{ borderBottom:'none' }}> {int(retrofitTotal)}{units.CR}
{translate('retrofit from')}
; } /** * 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
{rows}
{translate('module')} {translate('qty')} {translate('unit cost')} {translate('subtotal')}
{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}
); } }