From ab0019424fb781460aa0b72d15864a6b2d79bc72 Mon Sep 17 00:00:00 2001 From: Colin McLeod Date: Sun, 13 Dec 2015 11:51:58 -0800 Subject: [PATCH] More refactoring and porting to React --- .eslintrc | 43 +- devServer.js | 7 +- package.json | 10 +- src/app/Coriolis.jsx | 56 +- src/app/Router.js | 95 ++- src/app/components/AvailableModulesMenu.jsx | 7 +- src/app/components/CostSection.jsx | 435 +++++++++++++ src/app/components/HardpointSlot.jsx | 12 +- src/app/components/HardpointsSlotSection.jsx | 20 +- src/app/components/Header.jsx | 38 +- src/app/components/InternalSlot.jsx | 2 +- src/app/components/InternalSlotSection.jsx | 61 +- src/app/components/LineChart.jsx | 51 ++ src/app/components/ModalExport.jsx | 12 +- src/app/components/ModalImport.jsx | 7 +- src/app/components/ModalPermalink.jsx | 4 +- src/app/components/PowerBands.jsx | 244 +++++++ src/app/components/PowerManagement.jsx | 179 ++++++ src/app/components/ShipSummaryTable.jsx | 4 - src/app/components/Slot.jsx | 41 +- src/app/components/SlotSection.jsx | 54 +- src/app/components/StandardSlot.jsx | 6 +- src/app/components/StandardSlotSection.jsx | 105 ++- src/app/components/SvgIcons.jsx | 2 + src/app/components/UtilitySlotSection.jsx | 33 +- src/app/components/directive-area-chart.js | 164 ----- src/app/components/directive-power-bands.js | 209 ------ src/app/i18n/Language.jsx | 38 +- src/app/i18n/en.js | 323 +++++----- src/app/i18n/ru.js | 1 - src/app/pages/ComparisonPage.jsx | 76 +-- src/app/pages/OutfittingPage.jsx | 176 +++-- src/app/pages/Page.jsx | 6 +- src/app/pages/ShipyardPage.jsx | 141 ++-- src/app/pages/controller-outfit.js | 641 ------------------- src/app/shipyard/Constants.js | 175 ++--- src/app/shipyard/ModuleSet.js | 5 +- src/app/shipyard/ModuleUtils.js | 36 +- src/app/shipyard/Modules.js | 96 --- src/app/shipyard/Serializer.js | 140 +--- src/app/shipyard/Ship.js | 259 ++++++-- src/app/shipyard/Ships.js | 59 -- src/app/stores/Persist.js | 21 +- src/app/utils/BBCode.js | 21 +- src/app/utils/InterfaceEvents.js | 43 +- src/app/utils/ShortenUrl.js | 10 +- src/app/utils/SlotFunctions.js | 19 + src/app/utils/shallowEqual.js | 17 +- src/images/logo/browserconfig.xml | 8 +- src/images/logo/manifest.json | 8 +- src/index.html | 50 +- src/less/app.less | 7 +- src/less/header.less | 7 +- src/less/outfit.less | 15 +- src/less/table.less | 15 +- src/views/page-outfit.html | 408 ------------ webpack.config.dev.js | 11 +- webpack.config.prod.js | 17 +- 58 files changed, 2243 insertions(+), 2507 deletions(-) create mode 100644 src/app/components/CostSection.jsx create mode 100644 src/app/components/LineChart.jsx create mode 100644 src/app/components/PowerBands.jsx create mode 100644 src/app/components/PowerManagement.jsx delete mode 100755 src/app/components/directive-area-chart.js delete mode 100644 src/app/components/directive-power-bands.js delete mode 100755 src/app/pages/controller-outfit.js delete mode 100644 src/app/shipyard/Modules.js delete mode 100644 src/app/shipyard/Ships.js create mode 100644 src/app/utils/SlotFunctions.js delete mode 100644 src/views/page-outfit.html diff --git a/.eslintrc b/.eslintrc index 329eec0b..6be7aa40 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,21 +1,52 @@ { + "parser": "babel-eslint", "ecmaFeatures": { "jsx": true, + "classes": true, "modules": true }, "env": { "browser": true, "node": true }, - "parser": "babel-eslint", + "plugins": [ + "react" + ], "rules": { + "strict": 0, + "no-underscore-dangle": 0, + "valid-jsdoc": [2, { + "requireReturn": false + }], + "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "comma-style": [2, "last"], + "indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }], "quotes": [2, "single"], - "strict": [2, "never"], + "no-spaced-func": 2, + "operator-linebreak": [2, "after"], + "padded-blocks": [2, "never"], + "semi": [2, "always"], + "no-undef": 2, + "semi-spacing": [2, { "before": false, "after": true }], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "never"], + "object-curly-spacing": [2, "always"], + "array-bracket-spacing": [2, "never"], + "computed-property-spacing": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "spaced-comment": [2, "always"], + "no-var": 2, + "object-shorthand": [2, "always"], "react/jsx-uses-react": 2, "react/jsx-uses-vars": 2, "react/react-in-jsx-scope": 2 - }, - "plugins": [ - "react" - ] + } } diff --git a/devServer.js b/devServer.js index 1e65a9ff..eb8d8c80 100644 --- a/devServer.js +++ b/devServer.js @@ -5,7 +5,12 @@ var config = require('./webpack.config.dev'); new WebpackDevServer(webpack(config), { publicPath: config.output.publicPath, hot: true, - historyApiFallback: true + historyApiFallback: { + rewrites: [ + // For some reason connect-history-api-fallback does not allow '.' in the URL for history fallback... + { from: /\/outfit\//, to: '/index.html' } + ] + } }).listen(3300, "0.0.0.0", function (err, result) { if (err) { console.log(err); diff --git a/package.json b/package.json index 78966574..f0fc3d72 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,10 @@ "scripts": { "clean": "rimraf build", "start": "node devServer.js", - "lint": "eslint ./src", + "lint": "eslint --ext .js,.jsx src", "prod-serve": "nginx -p $(pwd) -c nginx.conf", "prod-stop": "kill -QUIT $(cat nginx.pid)", + "build:prod": "npm run clean && NODE_ENV=production CDN='//cdn.coriolis.io' webpack -d -p --config webpack.config.prod.js", "build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js", "rsync": "rsync -e 'ssh -i $CORIOLIS_PEM' -a --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/www", "deploy": "npm run lint && npm run build && npm run rsync" @@ -23,13 +24,15 @@ "devDependencies": { "appcache-webpack-plugin": "^1.2.1", "babel-core": "^5.4.7", - "babel-eslint": "^3.1.9", + "babel-eslint": "^4.1.6", "babel-loader": "^5.1.2", "babel-plugin-react-transform": "^1.1.1", "css-loader": "^0.23.0", - "eslint": "^1.3.1", + "eslint": "^1.10.1", "eslint-plugin-react": "^2.3.0", + "expose-loader": "^0.7.1", "express": "^4.13.3", + "extract-text-webpack-plugin": "^0.9.1", "file-loader": "^0.8.4", "html-webpack-plugin": "^1.7.0", "json-loader": "^0.5.3", @@ -47,7 +50,6 @@ "dependencies": { "classnames": "^2.2.0", "d3": "^3.5.9", - "extract-text-webpack-plugin": "^0.9.1", "fbemitter": "^2.0.0", "lz-string": "^1.4.4", "react": "^0.14.2", diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index 466cd826..5f298f60 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -20,6 +20,12 @@ export default class Coriolis extends React.Component { constructor() { super(); this._setPage = this._setPage.bind(this); + this._openMenu = this._openMenu.bind(this); + this._closeMenu = this._closeMenu.bind(this); + this._showModal = this._showModal.bind(this); + this._hideModal = this._hideModal.bind(this); + this._onLanguageChange = this._onLanguageChange.bind(this) + this._keyDown = this._keyDown.bind(this); this.state = { page: null, @@ -28,22 +34,20 @@ export default class Coriolis extends React.Component { }; Router('', (r) => this._setPage(ShipyardPage, r)); - // Router('/', (ctx) => this._setPage(ShipyardPage, ctx)); - Router('/outfitting/:ship', (r) => this._setPage(OutfittingPage, r)); - Router('/outfitting/:ship/:code', (r) => this._setPage(OutfittingPage, r)); + Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r)); // Router('/compare/:name', compare); // Router('/comparison/:code', comparison); - // Router('/settings', settings); Router('/about', (r) => this._setPage(AboutPage, r)); Router('*', (r) => this._setPage(null, r)); } _setPage(page, route) { - this.setState({ page, route }); + this.setState({ page, route, currentMenu: null }); } _onError(msg, scriptUrl, line, col, errObj) { - this._setPage(
Some errors occured!!
); + console.log('WINDOW ERROR', arguments); + //this._setPage(
Some errors occured!!
); } _onLanguageChange(lang) { @@ -53,8 +57,8 @@ export default class Coriolis extends React.Component { _keyDown(e) { switch (e.keyCode) { case 27: - InterfaceEvents.closeAll(); this._hideModal(); + this._closeMenu(); break; } } @@ -70,6 +74,18 @@ export default class Coriolis extends React.Component { } } + _openMenu(currentMenu) { + if (this.state.currentMenu != currentMenu) { + this.setState({ currentMenu }); + } + } + + _closeMenu() { + if (this.state.currentMenu) { + this.setState({ currentMenu: null }); + } + } + getChildContext() { return { language: this.state.language, @@ -82,28 +98,30 @@ export default class Coriolis extends React.Component { if (window.applicationCache) { window.applicationCache.addEventListener('updateready', () => { if (window.applicationCache.status == window.applicationCache.UPDATEREADY) { - this.setState({appCacheUpdate: true}); // Browser downloaded a new app cache. + this.setState({ appCacheUpdate: true }); // Browser downloaded a new app cache. } - }, false); + }); } window.onerror = this._onError.bind(this); - document.addEventListener('keydown', this._keyDown.bind(this)); - Persist.addListener('language', this._onLanguageChange.bind(this)); - Persist.addListener('language', this._onLanguageChange.bind(this)); - InterfaceEvents.addListener('showModal', this._showModal.bind(this)); - InterfaceEvents.addListener('hideModal', this._hideModal.bind(this)); + window.addEventListener('resize', InterfaceEvents.windowResized); + document.addEventListener('keydown', this._keyDown); + Persist.addListener('language', this._onLanguageChange); + Persist.addListener('language', this._onLanguageChange); + InterfaceEvents.addListener('openMenu', this._openMenu); + InterfaceEvents.addListener('closeMenu', this._closeMenu); + InterfaceEvents.addListener('showModal', this._showModal); + InterfaceEvents.addListener('hideModal', this._hideModal); Router.start(); } - render() { return ( -
-
- {this.state.page? : } - {this.state.modal} +
+
+ { this.state.page ? : } + { this.state.modal }
); } diff --git a/src/app/Router.js b/src/app/Router.js index c5929c7d..edd8b389 100644 --- a/src/app/Router.js +++ b/src/app/Router.js @@ -1,5 +1,4 @@ import Persist from './stores/Persist'; -import InterfaceEvents from './utils/InterfaceEvents'; function isStandAlone() { try { @@ -49,7 +48,7 @@ Router.start = function(){ Router('/'); } } else { - var url = location.pathname + location.search + location.hash; + var url = location.pathname + location.search; Router.replace(url, null, true, true); } }; @@ -64,10 +63,11 @@ Router.start = function(){ */ Router.go = function(path, state) { gaTrack(path); - InterfaceEvents.closeAll(); var ctx = new Context(path, state); Router.dispatch(ctx); - if (!ctx.unhandled) ctx.pushState(); + if (!ctx.unhandled) { + history.pushState(ctx.state, ctx.title, ctx.canonicalPath); + } return ctx; }; @@ -80,12 +80,11 @@ Router.go = function(path, state) { * @api public */ -Router.replace = function(path, state, init, dispatch) { +Router.replace = function(path, state, dispatch) { gaTrack(path); var ctx = new Context(path, state); - ctx.init = init; - if (dispatch !== false) Router.dispatch(ctx); - ctx.save(); + if (dispatch) Router.dispatch(ctx); + history.replaceState(ctx.state, ctx.title, ctx.canonicalPath); return ctx; }; @@ -119,8 +118,10 @@ Router.dispatch = function(ctx){ function unhandled(ctx) { var current = window.location.pathname + window.location.search; - if (current == ctx.canonicalPath) return; - window.location = ctx.canonicalPath; + if (current != ctx.canonicalPath) { + window.location = ctx.canonicalPath; + } + return ctx; } /** @@ -142,43 +143,14 @@ function Context(path, state) { this.state.path = path; this.querystring = ~i ? path.slice(i + 1) : ''; this.pathname = ~i ? path.slice(0, i) : path; - this.params = []; + this.params = {}; - // fragment - this.hash = ''; - if (!~this.path.indexOf('#')) return; - var parts = this.path.split('#'); - this.path = parts[0]; - this.hash = parts[1] || ''; - this.querystring = this.querystring.split('#')[0]; + this.querystring.split('&').forEach((str) =>{ + let query = str.split('='); + this.params[query[0]] = decodeURIComponent(query[1]); + }, this); } -/** - * Expose `Context`. - */ - -Router.Context = Context; - -/** - * Push state. - * - * @api private - */ - -Context.prototype.pushState = function(){ - history.pushState(this.state, this.title, this.canonicalPath); -}; - -/** - * Save the context state. - * - * @api public - */ - -Context.prototype.save = function(){ - history.replaceState(this.state, this.title, this.canonicalPath); -}; - /** * Initialize `Route` with the given HTTP `path`, * and an array of `callbacks` and `options`. @@ -203,12 +175,6 @@ function Route(path, options) { , options.strict); } -/** - * Expose `Route`. - */ - -Router.Route = Route; - /** * Return route middleware with * the given callback `fn()`. @@ -247,22 +213,33 @@ Route.prototype.match = function(path, params){ for (var i = 1, len = m.length; i < len; ++i) { var key = keys[i - 1]; - var val = 'string' == typeof m[i] - ? decodeURIComponent(m[i]) - : m[i]; + var val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; if (key) { - params[key.name] = undefined !== params[key.name] - ? params[key.name] - : val; - } else { - params.push(val); + params[key.name] = undefined !== params[key.name] ? params[key.name] : val; } } return true; }; + +/** + * Check if the app is running in stand alone mode. + * @return {Boolean} true if running in Standalone mode + */ +function isStandAlone() { + try { + return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode()); + } catch (ex) { + return false; + } +} + +/** + * Track a page view in Google Analytics + * @param {string} path + */ function gaTrack(path) { if (window.ga) { window.ga('send', 'pageview', { page: path }); @@ -314,7 +291,7 @@ function pathtoRegexp(path, keys, sensitive, strict) { function onpopstate(e) { if (e.state) { var path = e.state.path; - Router.replace(path, e.state); + Router.replace(path, e.state, true); } } diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index 68fc3312..ea30de3e 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -31,13 +31,13 @@ export default class AvailableModulesMenu extends TranslatedComponent { disabled: m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass }); - switch(m.mode) { + switch(m.mount) { case 'F': mount = ; break; case 'G': mount = ; break; case 'T': mount = ; break; } - if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mode) && m.grp != 'pa') { + if (i > 0 && modules.length > 3 && m.class != prevClass && (m.rating != prevRating || m.mount) && m.grp != 'pa') { elems.push(
); } @@ -77,15 +77,12 @@ export default class AvailableModulesMenu extends TranslatedComponent { ); if (modules instanceof Array) { - console.log(modules[0].grp, modules); list = buildGroup(modules[0].grp, modules); } else { - console.log('menu object') list = []; // At present time slots with grouped options (Hardpoints and Internal) can be empty list.push(
{translate('empty')}
); for (let g in modules) { - let grp = modules[g]; list.push(
{translate(g)}
); list.push(buildGroup(g, modules[g])); } diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx new file mode 100644 index 00000000..ae226b30 --- /dev/null +++ b/src/app/components/CostSection.jsx @@ -0,0 +1,435 @@ +import React from 'react'; +import cn from 'classnames'; +import { Ships } from 'coriolis-data'; +import Persist from '../stores/Persist'; +import Ship from '../shipyard/Ship'; +import { Insurance } from '../shipyard/Constants'; +import { slotName, nameComparator } from '../utils/SlotFunctions'; +import TranslatedComponent from './TranslatedComponent'; + +export default class CostSection extends TranslatedComponent { + + static PropTypes = { + ship: React.PropTypes.object.isRequired, + shipId: React.PropTypes.string.isRequired, + code: React.PropTypes.string.isRequired, + buildName: React.PropTypes.string + }; + + constructor(props) { + super(props); + + this._costsTab = this._costsTab.bind(this); + + let data = Ships[props.shipId]; // Retrieve the basic ship properties, slots and defaults + let retrofitName = props.buildName; + let shipDiscount = Persist.getShipDiscount(); + let moduleDiscount = Persist.getModuleDiscount(); + let existingBuild = Persist.getBuild(props.shipId, retrofitName); + let retrofitShip = new Ship(props.shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison + + if (existingBuild) { + retrofitShip.buildFrom(existingBuild); // Populate modules from existing build + } else { + retrofitShip.buildWith(data.defaults); // Populate with default components + } + + this.props.ship.applyDiscounts(shipDiscount, moduleDiscount); + retrofitShip.applyDiscounts(shipDiscount, moduleDiscount); + + this.state = { + retrofitShip, + retrofitName, + shipDiscount, + moduleDiscount, + total: props.ship.totalCost, + insurance: Insurance[Persist.getInsurance()], + tab: Persist.getCostTab(), + buildOptions: Persist.getBuildsNamesFor(props.shipId), + ammoPredicate: 'module', + ammoDesc: true, + costPredicate: 'cr', + costDesc: true, + retroPredicate: 'module', + retroDesc: true + }; + } + + _showTab(tab) { + Persist.setCostTab(tab); + this.setState({ tab }); + } + + _onDiscountChanged() { + let shipDiscount = Persist.getShipDiscount(); + let moduleDiscount = Persist.getModuleDiscount(); + this.props.ship.applyDiscounts(shipDiscount, moduleDiscount); + this.state.retrofitShip.applyDiscounts(shipDiscount, moduleDiscount); + this.setState({ shipDiscount, moduleDiscount }); + } + + _onInsuranceChanged(insuranceName) { + this.setState({ insurance: Insurance[insuranceName] }); + } + + _onBaseRetrofitChange(retrofitName) { + let existingBuild = Persist.getBuild(this.props.shipId, retrofitName); + this.state.retrofitShip.buildFrom(existingBuild); // Repopulate modules from existing build + this.setState({ retrofitName }); + } + + _onBuildSaved(shipId, name, code) { + if(this.state.retrofitName == name) { + this.state.retrofitShip.buildFrom(code); // Repopulate modules from saved build + } else { + this.setState({buildOptions: Persist.getBuildsNamesFor(this.props.shipId) }); + } + } + + _toggleCost(item) { + this.props.ship.setCostIncluded(item, !item.incCost); + this.setState({ total: this.props.ship.totalCost }); + } + + _sortCost(predicate) { + let costList = this.props.ship.costList; + let { costPredicate, costDesc } = this.state; + + if (predicate) { + if (costPredicate == predicate) { + costDesc = !costDesc; + } + } else { + predicate = costPredicate; + } + + if (predicate == 'm') { + let translate = this.context.language.translate; + costList.sort(nameComparator(translate)); + } else { + costList.sort((a, b) => (a.m && a.m.cost ? a.m.cost : 0) - (b.m && b.m.cost ? b.m.cost : 0)); + } + + if (!costDesc) { + costList.reverse(); + } + + this.setState({ costPredicate: predicate, costDesc }); + } + + _sortAmmo(predicate) { + let { ammoPredicate, ammoDesc, ammoCosts } = this.state; + + if (ammoPredicate == predicate) { + ammoDesc = !ammoDesc; + } + + switch (predicate) { + case 'm': + let translate = this.context.language.translate; + ammoCosts.sort(nameComparator(translate)); + break; + default: + ammoCosts.sort((a, b) => a[predicate] - b[predicate]); + } + + if (!ammoDesc) { + ammoCosts.reverse(); + } + + this.setState({ ammoPredicate: predicate, ammoDesc }); + } + + _costsTab() { + let { ship } = this.props; + let { total, 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('component')} + {shipDiscount < 1 && {`[${translate('ship')} -${formats.rPct(1 - shipDiscount)}]`}} + {moduleDiscount < 1 && {`[${translate('modules')} -${formats.rPct(1 - moduleDiscount)}]`}} + {translate('credits')}
{translate('total')}{formats.int(total)}{units.CR}
{translate('insurance')}{formats.int(total * insurance)}{units.CR}
+
; + } + + updateRetrofitCosts() { + var costs = $scope.retrofitList = []; + var total = 0, i, l, item; + + if (ship.bulkheads.id != retrofitShip.bulkheads.id) { + item = { + buyClassRating: ship.bulkheads.m.class + ship.bulkheads.m.rating, + 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 + }; + costs.push(item); + if (retrofitShip.bulkheads.incCost) { + total += item.netCost; + } + } + + for (var g in { standard: 1, internal: 1, hardpoints: 1 }) { + var retroSlotGroup = retrofitShip[g]; + var slotGroup = ship[g]; + for (i = 0, l = slotGroup.length; i < l; i++) { + if (slotGroup[i].id != retroSlotGroup[i].id) { + item = { netCost: 0, retroItem: retroSlotGroup[i] }; + if (slotGroup[i].id) { + 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].id) { + 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; + } + costs.push(item); + if (retroSlotGroup[i].incCost) { + total += item.netCost; + } + } + } + } + $scope.retrofitTotal = total; + } + + _retrofitTab() { + // return
+ //
+ // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + // + //
{translate('sell')}{translate('buy')} + // {{'net cost' | translate}} [-{{fRPct(1 - discounts.components)}}] + //
{translate('PHRASE_NO_RETROCH')}
{{item.sellClassRating}}{{item.sellName | translate}}{{item.buyClassRating}}{{item.buyName | translate}} 0 ? 'warning' : 'secondary-disabled' : 'disabled' )}>{{ fCrd(item.netCost)}} CR
+ //
+ // + // + // + // + // + // + // + // + // + // + //
{translate('cost')} 0 ? 'warning' : 'secondary-disabled'}>{{fCrd(retrofitTotal)}} CR
{translate('retrofit from')} + // + //
+ //
; + } + + _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} + ); + } + console.log(rows); + return
+
+ + + + + + + + + + + {rows} + + + + + +
{translate('module')}{translate('qty')}{translate('unit cost')}{translate('total cost')}
{translate('total')}{int(ammoTotal)}{units.CR}
+
+
; + } + + /** + * Recalculate all ammo costs + */ + _updateAmmoCosts() { + let ship = this.props.ship; + let ammoCosts = [], ammoTotal = 0, item, q, limpets = 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].id) { + //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.cells; + break; + case 'am': + q = slotGroup[i].m.ammo; + break; + case 'fx': case 'hb': case 'cc': case 'pc': + limpets = ship.cargoCapacity; + break; + default: + q = slotGroup[i].m.clip + slotGroup[i].m.ammo; + } + //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; + } + } + } + } + + //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; + } + //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 }); + } + + componentWillMount(){ + this.listeners = [ + Persist.addListener('discounts', this._onDiscountChanged.bind(this)), + Persist.addListener('insurance', this._onInsuranceChanged.bind(this)), + ]; + this._updateAmmoCosts(this.props.ship); + this._sortCost.call(this, this.state.costPredicate); + } + + componentWillReceiveProps(nextProps, nextContext) { + this._updateAmmoCosts(nextProps.ship); + + this._sortCost(); + } + + componentWillUnmount(){ + // remove window listener + this.listeners.forEach(l => l.remove()); + } + + 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} +
+ ); + } +} diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 6c4dfab6..13abf4b8 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -3,17 +3,17 @@ import Slot from './Slot'; export default class HardpointSlot extends Slot { - getClassNames() { - return this.props.size > 0 ? 'hardpoint' : null; + _getClassNames() { + return this.props.maxClass > 0 ? 'hardpoint' : null; } - getSize(translate){ - return translate(['U','S','M','L','H'][this.props.size]); + _getMaxClassLabel(translate){ + return translate(['U','S','M','L','H'][this.props.maxClass]); } - getSlotDetails(m, translate, formats, u) { + _getSlotDetails(m, translate, formats, u) { if (m) { - let classRating = `${m.class}${m.rating}${m.mode ? '/' + m.mode : ''}${m.missile ? m.missile : ''}`; + let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`; return (
{classRating + ' ' + translate(m.name || m.grp)}
diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx index b4e3f386..ce7bd3ef 100644 --- a/src/app/components/HardpointsSlotSection.jsx +++ b/src/app/components/HardpointsSlotSection.jsx @@ -13,27 +13,35 @@ export default class HardpointsSlotSection extends SlotSection { } _empty() { - + this.props.ship.emptyWeapons(); + this.props.onChange(); + this._close(); } - _fill(grp, mount) { + _fill(group, mount, event) { + this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt')); + this.props.onChange(); + this._close(); + } + _contextMenu() { + this._empty(); } _getSlots() { let slots = []; let hardpoints = this.props.ship.hardpoints; let availableModules = this.props.ship.getAvailableModules(); - let currentMenu = this.state.currentMenu; + let currentMenu = this.props.currentMenu; for (let i = 0, l = hardpoints.length; i < l; i++) { let h = hardpoints[i]; if (h.maxClass) { slots.push( availableModules.getHps(h.maxClass)} + onOpen={this._openMenu.bind(this, h)} onSelect={this._selectModule.bind(this, h)} selected={currentMenu == h} m={h.m} diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index 875bbb99..da8511b9 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -4,7 +4,7 @@ import Link from './Link'; import ActiveLink from './ActiveLink'; import cn from 'classnames'; import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons'; -import Ships from '../shipyard/Ships'; +import { Ships } from 'coriolis-data'; import InterfaceEvents from '../utils/InterfaceEvents'; import Persist from '../stores/Persist'; import ModalDeleteAll from './ModalDeleteAll'; @@ -13,21 +13,12 @@ export default class Header extends TranslatedComponent { constructor(props) { super(props); - this.state = { - openedMenu: null - }; - - this._close = this._close.bind(this); this.shipOrder = Object.keys(Ships).sort(); } - _close() { - this.setState({ openedMenu: null }); - } - _setInsurance(e) { e.stopPropagation(); - Persist.setInsurance('Beta'); // TODO: get insurance name + Persist.setInsurance('beta'); // TODO: get insurance name } _setModuleDiscount(e) { @@ -43,7 +34,6 @@ export default class Header extends TranslatedComponent { _showDeleteAll(e) { e.preventDefault(); InterfaceEvents.showModal(); - this._close(); }; _showBackup(e) { @@ -76,21 +66,21 @@ export default class Header extends TranslatedComponent { Persist.setSizeRatio(1); } - _openMenu(event, openedMenu) { + _openMenu(event, menu) { event.stopPropagation(); - if (this.state.openedMenu == openedMenu) { - openedMenu = null; + if (this.props.currentMenu == menu) { + menu = null; } - this.setState({ openedMenu }); + InterfaceEvents.openMenu(menu); } _getShipsMenu() { let shipList = []; for (let s in Ships) { - shipList.push({Ships[s].properties.name}); + shipList.push({Ships[s].properties.name}); } return ( @@ -108,7 +98,7 @@ export default class Header extends TranslatedComponent { let shipBuilds = []; let buildNameOrder = Object.keys(builds[shipId]).sort(); for (let buildName of buildNameOrder) { - let href = ['/outfitting/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join(''); + let href = ['/outfit/', shipId, '/', builds[shipId][buildName], '?bn=', buildName].join(''); shipBuilds.push(
  • {buildName}
  • ); } buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); @@ -196,21 +186,13 @@ export default class Header extends TranslatedComponent { ); } - componentWillMount() { - this.closeAllListener = InterfaceEvents.addListener('closeAll', this._close); - } - - componentWillUnmount() { - this.closeAllListener.remove(); - } - render() { let translate = this.context.language.translate; - let openedMenu = this.state.openedMenu; + let openedMenu = this.props.currentMenu; let hasBuilds = Persist.hasBuilds(); if (this.props.appCacheUpdate) { - return
    { 'PHRASE_UPDATE_RDY' | translate }
    ; + return
    window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}
    ; } return ( diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index dcf30582..cff5d53e 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -4,7 +4,7 @@ import { Infinite } from './SvgIcons'; export default class InternalSlot extends Slot { - getSlotDetails(m, translate, formats, u) { + _getSlotDetails(m, translate, formats, u) { if (m) { let classRating = m.class + m.rating; diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx index 28b8c793..079edd8b 100644 --- a/src/app/components/InternalSlotSection.jsx +++ b/src/app/components/InternalSlotSection.jsx @@ -1,7 +1,8 @@ import React from 'react'; +import cn from 'classnames'; import SlotSection from './SlotSection'; import InternalSlot from './InternalSlot'; -import cn from 'classnames'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; export default class InternalSlotSection extends SlotSection { @@ -16,36 +17,70 @@ export default class InternalSlotSection extends SlotSection { } _empty() { - + this.props.ship.emptyInternal(); + this.props.onChange(); + this._close(); } - _fillWithCargo() { - + _fillWithCargo(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.m) { + ship.use(slot, ModuleUtils.findInternal('cr', slot.maxClass, 'E')); + } + }); + this.props.onChange(); + this._close(); } - _fillWithCells() { - + _fillWithCells(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + let chargeCap = 0; // Capacity of single activation + ship.internal.forEach(function(slot) { + if ((!slot.m || (clobber && !ModuleUtils.isShieldGenerator(slot.m.grp))) && (!slot.eligible || slot.eligible.scb)) { // Check eligibility due to Orca special case + ship.use(slot, ModuleUtils.findInternal('scb', slot.maxClass, 'A')); + ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge + chargeCap += slot.m.recharge; + } + }); + this.props.onChange(); + this._close(); } - _fillWithArmor() { + _fillWithArmor(event) { + let clobber = event.getModifierState('Alt'); + let ship = this.props.ship; + ship.internal.forEach((slot) => { + if (clobber || !slot.c) { + ship.use(slot, ModuleUtils.findInternal('hr', Math.min(slot.maxClass, 5), 'D')); // Hull reinforcements top out at 5D + } + }); + this.props.onChange(); + this._close(); + } + _contextMenu() { + this._empty(); } _getSlots() { let slots = []; - let {internal, fuelCapacity, ladenMass } = this.props.ship; - let availableModules = this.props.ship.getAvailableModules(); - let currentMenu = this.state.currentMenu; + let { currentMenu, ship } = this.props; + let {internal, fuelCapacity, ladenMass } = ship; + let availableModules = ship.getAvailableModules(); for (let i = 0, l = internal.length; i < l; i++) { let s = internal[i]; slots.push( availableModules.getInts(s.maxClass, s.eligible)} onOpen={this._openMenu.bind(this,s)} - onSelect={this._selectModule.bind(this, i, s)} + onSelect={this._selectModule.bind(this, s)} selected={currentMenu == s} + enabled={s.enabled} m={s.m} fuel={fuelCapacity} shipMass={ladenMass} diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx new file mode 100644 index 00000000..21512762 --- /dev/null +++ b/src/app/components/LineChart.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; + +const RENDER_POINTS = 20; // Only render 20 points on the graph + +export default class LineChart extends TranslatedComponent { + + static defaultProps = { + xMin: 0, + yMin: 0, + colors: ['#ff8c0d'] + } + + static PropTypes = { + xMax: React.PropTypes.number.isRequired, + yMax: React.PropTypes.number.isRequired, + func: React.PropTypes.func.isRequired, + series: React.PropTypes.array, + colors: React.PropTypes.array, + xMin: React.PropTypes.number, + yMin: React.PropTypes.number, + xUnit: React.PropTypes.string, + yUnit: React.PropTypes.string, + xLabel: React.PropTypes.string, + xLabel: React.PropTypes.string, + }; + + constructor(props) { + super(props); + + // init + + } + + componentWillMount(){ + // Listen to window resize + } + + componentWillUnmount(){ + // remove window listener + // remove mouse move listener / touch listner? + } + + componentWillReceiveProps(nextProps, nextContext) { + // on language change update formatting + } + + render() { + return
    ; + } +} diff --git a/src/app/components/ModalExport.jsx b/src/app/components/ModalExport.jsx index db966748..f84c3d10 100644 --- a/src/app/components/ModalExport.jsx +++ b/src/app/components/ModalExport.jsx @@ -2,12 +2,12 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import InterfaceEvents from '../utils/InterfaceEvents'; -export default class DeleteAllModal extends TranslatedComponent { +export default class ModalExport extends TranslatedComponent { static propTypes = { - title: React.propTypes.string, - promise: : React.propTypes.func, - data: React.propTypes.oneOfType[React.propTypes.string, React.propTypes.object] + title: React.PropTypes.string, + promise: React.PropTypes.func, + data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object]) }; constructor(props) { @@ -19,7 +19,7 @@ export default class DeleteAllModal extends TranslatedComponent { } else if(typeof props.data == 'string') { exportJson = props.data; } else { - exportJson = JSON.stringify(this.props.data); + exportJson = JSON.stringify(this.props.data, null, 2); } this.state = { exportJson }; @@ -41,7 +41,7 @@ export default class DeleteAllModal extends TranslatedComponent {

    {translate(this.props.title || 'Export')}

    {description}
    - +