diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..31749320 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015", "react", "stage-0"] +} \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..683bc59a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,61 @@ +{ + "parser": "babel-eslint", + "parserOptions": { + "ecmaFeatures": { + "jsx": true, + "classes": true, + "modules": true + }, + }, + "env": { + "browser": true, + "node": true + }, + "plugins": [ + "react" + ], + "extends": "plugin:react/recommended", + "settings": { + "react": { + "pragma": "React" + } + }, + "rules": { + "strict": 0, + "no-underscore-dangle": 0, + "valid-jsdoc": [2, { + "requireReturn": false + }], + "require-jsdoc": [2, { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true + } + }], + "no-console": 2, + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "comma-style": [2, "last"], + "indent": [2, 2, { "SwitchCase": 1, "VariableDeclarator": 2 }], + "quotes": [2, "single"], + "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 + } +} diff --git a/.gitignore b/.gitignore index a06816f3..e5c4dc84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,5 @@ node_modules -bower_components -bower_components/* build .DS_Store *.log -app/js/db.js -app/db.json nginx.pid -template_cache.js \ No newline at end of file diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 16fd4b05..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "data"] - path = data - url = https://github.com/cmmcleod/coriolis-data.git diff --git a/.travis.yml b/.travis.yml index 52561dc0..5240eef1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,17 +3,13 @@ notifications: email: false sudo: false node_js: - - "0.12" + - "4.2.6" cache: directories: - node_modules - - bower_components before_script: - - npm install -g gulp - - npm install -g bower - - bower install + script: - - gulp lint - - gulp build-prod - - gulp test \ No newline at end of file + - npm run lint + - npm test \ No newline at end of file diff --git a/README.md b/README.md index 9aea0b4b..f24d579e 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -[![Build Status](https://travis-ci.org/cmmcleod/coriolis.svg?branch=master)](https://travis-ci.org/cmmcleod/coriolis) [![Tasks in Ready](https://badge.waffle.io/cmmcleod/coriolis.png?label=ready&title=Ready)](https://waffle.io/cmmcleod/coriolis) [![Tasks in Progress](https://badge.waffle.io/cmmcleod/coriolis.svg?label=in%20progress&title=In%20Progress)](http://waffle.io/cmmcleod/coriolis) - +![Latest Release](https://img.shields.io/github/release/cmmcleod/coriolis.svg) [![Build Status](https://travis-ci.org/cmmcleod/coriolis.svg?branch=master)](https://travis-ci.org/cmmcleod/coriolis) [![Tasks in Ready](https://badge.waffle.io/cmmcleod/coriolis.png?label=ready&title=Ready)](https://waffle.io/cmmcleod/coriolis) [![Tasks in Progress](https://badge.waffle.io/cmmcleod/coriolis.svg?label=in%20progress&title=In%20Progress)](http://waffle.io/cmmcleod/coriolis) [![Chat to us on HipChat](https://img.shields.io/badge/HipChat-Coriolis-white.svg?style=social)](https://www.hipchat.com/gfYQiZcmy) ## About @@ -14,6 +13,8 @@ Please [submit issues](https://github.com/cmmcleod/coriolis/issues), or better y ### Feature Requests, Suggestions & Bugs +Chat to us on [HipChat](https://www.hipchat.com/gfYQiZcmy)! + All such requests are managed and tracked through [issues](https://github.com/cmmcleod/coriolis/issues). An overview of these can be found [here](https://waffle.io/cmmcleod/coriolis). ## Development @@ -21,7 +22,7 @@ All such requests are managed and tracked through [issues](https://github.com/cm See the [Developer's Guide](https://github.com/cmmcleod/coriolis/wiki/Developer's-Guide) in the wiki. -### Ship and Component Database +### Ship and Module Database See the [Data wiki](https://github.com/cmmcleod/coriolis-data/wiki) for details on structure, etc. diff --git a/test/fixtures/anaconda-test-detailed-export-v2.json b/__tests__/fixtures/anaconda-test-detailed-export-v3.json similarity index 94% rename from test/fixtures/anaconda-test-detailed-export-v2.json rename to __tests__/fixtures/anaconda-test-detailed-export-v3.json index e51408b7..2617af65 100644 --- a/test/fixtures/anaconda-test-detailed-export-v2.json +++ b/__tests__/fixtures/anaconda-test-detailed-export-v3.json @@ -1,12 +1,12 @@ { - "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/2.json#", - "name": "Test", + "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/3.json#", + "name": "Test My Ship", "ship": "Anaconda", "references": [ { "name": "Coriolis.io", - "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA%3D%3D%3D.AwhMJBGaei%2BJCyyiA%3D%3D%3D?bn=Test", - "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18QDBNA===.AwhMJBGaei+JCyyiA===", + "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship", + "code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA", "shipId": "anaconda" } ], @@ -263,8 +263,7 @@ "hullMass": 400, "masslock": 23, "pipSpeed": 0.14, - "shipCostMultiplier": 1, - "componentCostMultiplier": 1, + "moduleCostMultiplier": 1, "fuelCapacity": 32, "cargoCapacity": 128, "ladenMass": 1339.2, @@ -281,8 +280,8 @@ "unladenRange": 18.49, "fullTankRange": 18.12, "ladenRange": 16.39, - "unladenTotalRange": 73.21, - "ladenTotalRange": 66.15, + "unladenFastestRange": 73.21, + "ladenFastestRange": 66.15, "maxJumpCount": 4, "shieldStrength": 833 } diff --git a/test/fixtures/ed-shipyard-import-invalid.json b/__tests__/fixtures/ed-shipyard-import-invalid.json similarity index 100% rename from test/fixtures/ed-shipyard-import-invalid.json rename to __tests__/fixtures/ed-shipyard-import-invalid.json diff --git a/test/fixtures/ed-shipyard-import-valid.json b/__tests__/fixtures/ed-shipyard-import-valid.json similarity index 100% rename from test/fixtures/ed-shipyard-import-valid.json rename to __tests__/fixtures/ed-shipyard-import-valid.json diff --git a/test/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json similarity index 100% rename from test/fixtures/expected-builds.json rename to __tests__/fixtures/expected-builds.json diff --git a/test/fixtures/old-valid-export.json b/__tests__/fixtures/old-valid-export.json similarity index 100% rename from test/fixtures/old-valid-export.json rename to __tests__/fixtures/old-valid-export.json diff --git a/test/fixtures/valid-backup.json b/__tests__/fixtures/valid-backup.json similarity index 98% rename from test/fixtures/valid-backup.json rename to __tests__/fixtures/valid-backup.json index 9067d141..683511e1 100644 --- a/test/fixtures/valid-backup.json +++ b/__tests__/fixtures/valid-backup.json @@ -16,7 +16,7 @@ "Miner": "25A5A5A4D4A5A5C0s0s24242l2l---04054a1q02022o27" }, "cobra_mk_iii": { - "Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ===", + "Example": "24A4A4A3D3A3A4C0s0s2d2d0m0445032b2o2753.AwRj4yKA.CwBhEYyrKhmMQ===" }, "imperial_clipper": { "Cargo": "03A5D5A5D4D5D4C--0s0s----0605450302020101", diff --git a/test/fixtures/valid-detailed-export.json b/__tests__/fixtures/valid-detailed-export.json similarity index 97% rename from test/fixtures/valid-detailed-export.json rename to __tests__/fixtures/valid-detailed-export.json index 1d2869b6..2906f78b 100644 --- a/test/fixtures/valid-detailed-export.json +++ b/__tests__/fixtures/valid-detailed-export.json @@ -117,8 +117,8 @@ "unladenRange": 32.48, "fullTankRange": 30.27, "ladenRange": 19.61, - "unladenTotalRange": 176.71, - "ladenTotalRange": 112.92, + "unladenFastestRange": 176.71, + "ladenFastestRange": 112.92, "maxJumpCount": 6, "shieldStrength": 86.49 } @@ -251,8 +251,8 @@ "unladenRange": 30.24, "fullTankRange": 28.32, "ladenRange": 19.8, - "unladenTotalRange": 164.89, - "ladenTotalRange": 114.03, + "unladenFastestRange": 164.89, + "ladenFastestRange": 114.03, "maxJumpCount": 6, "shieldStrength": 149.2 } @@ -381,8 +381,8 @@ "unladenRange": 31.71, "fullTankRange": 29.61, "ladenRange": 23.58, - "unladenTotalRange": 172.68, - "ladenTotalRange": 136.46, + "unladenFastestRange": 172.68, + "ladenFastestRange": 136.46, "maxJumpCount": 6, "shieldStrength": 86.49 } @@ -513,8 +513,8 @@ "unladenRange": 26.41, "fullTankRange": 24.97, "ladenRange": 17.36, - "unladenTotalRange": 172.04, - "ladenTotalRange": 118.55, + "unladenFastestRange": 172.04, + "ladenFastestRange": 118.55, "maxJumpCount": 7, "shieldStrength": 89.07 } @@ -655,8 +655,8 @@ "unladenRange": 25.77, "fullTankRange": 24.39, "ladenRange": 17.98, - "unladenTotalRange": 167.93, - "ladenTotalRange": 122.84, + "unladenFastestRange": 167.93, + "ladenFastestRange": 122.84, "maxJumpCount": 7, "shieldStrength": 125.07 } @@ -793,8 +793,8 @@ "unladenRange": 19.27, "fullTankRange": 18.95, "ladenRange": 15.43, - "unladenTotalRange": 67.34, - "ladenTotalRange": 54.75, + "unladenFastestRange": 67.34, + "ladenFastestRange": 54.75, "maxJumpCount": 4, "shieldStrength": 111.07 } @@ -956,8 +956,8 @@ "unladenRange": 27.1, "fullTankRange": 25.58, "ladenRange": 21.94, - "unladenTotalRange": 176.39, - "ladenTotalRange": 150.58, + "unladenFastestRange": 176.39, + "ladenFastestRange": 150.58, "maxJumpCount": 7, "shieldStrength": 253.58 } @@ -1098,8 +1098,8 @@ "unladenRange": 26.01, "fullTankRange": 25.42, "ladenRange": 17.19, - "unladenTotalRange": 90.67, - "ladenTotalRange": 61.04, + "unladenFastestRange": 90.67, + "ladenFastestRange": 61.04, "maxJumpCount": 4, "shieldStrength": 191.82 } @@ -1262,8 +1262,8 @@ "unladenRange": 15.19, "fullTankRange": 14.99, "ladenRange": 14.99, - "unladenTotalRange": 53.15, - "ladenTotalRange": 53.15, + "unladenFastestRange": 53.15, + "ladenFastestRange": 53.15, "maxJumpCount": 4, "shieldStrength": 489.6 } @@ -1362,8 +1362,8 @@ "unladenRange": 24.25, "fullTankRange": 23.73, "ladenRange": 23.73, - "unladenTotalRange": 84.56, - "ladenTotalRange": 84.56, + "unladenFastestRange": 84.56, + "ladenFastestRange": 84.56, "maxJumpCount": 4, "shieldStrength": 0 } @@ -1501,8 +1501,8 @@ "unladenRange": 19.51, "fullTankRange": 18.58, "ladenRange": 13.09, - "unladenTotalRange": 152.32, - "ladenTotalRange": 106.49, + "unladenFastestRange": 152.32, + "ladenFastestRange": 106.49, "maxJumpCount": 8, "shieldStrength": 170.57 } @@ -1639,8 +1639,8 @@ "unladenRange": 28.25, "fullTankRange": 26.6, "ladenRange": 16.67, - "unladenTotalRange": 183.67, - "ladenTotalRange": 113.69, + "unladenFastestRange": 183.67, + "ladenFastestRange": 113.69, "maxJumpCount": 7, "shieldStrength": 214.26 } @@ -1810,8 +1810,8 @@ "unladenRange": 20.32, "fullTankRange": 19.46, "ladenRange": 14.65, - "unladenTotalRange": 133.17, - "ladenTotalRange": 99.65, + "unladenFastestRange": 133.17, + "ladenFastestRange": 99.65, "maxJumpCount": 7, "shieldStrength": 486.35 } @@ -1989,8 +1989,8 @@ "unladenRange": 14.32, "fullTankRange": 13.89, "ladenRange": 13.46, - "unladenTotalRange": 94.42, - "ladenTotalRange": 91.49, + "unladenFastestRange": 94.42, + "ladenFastestRange": 91.49, "maxJumpCount": 7, "shieldStrength": 645.57 } @@ -2219,8 +2219,8 @@ "unladenRange": 16.99, "fullTankRange": 16.68, "ladenRange": 15.91, - "unladenTotalRange": 67.35, - "ladenTotalRange": 64.2, + "unladenFastestRange": 67.35, + "ladenFastestRange": 64.2, "maxJumpCount": 4, "shieldStrength": 952 } @@ -2374,8 +2374,8 @@ "unladenRange": 39.26, "fullTankRange": 37.65, "ladenRange": 21.21, - "unladenTotalRange": 153.79, - "ladenTotalRange": 85.82, + "unladenFastestRange": 153.79, + "ladenFastestRange": 85.82, "maxJumpCount": 4, "shieldStrength": 372.98 } @@ -2530,8 +2530,8 @@ "unladenRange": 38.44, "fullTankRange": 36.89, "ladenRange": 21.04, - "unladenTotalRange": 150.62, - "ladenTotalRange": 85.16, + "unladenFastestRange": 150.62, + "ladenFastestRange": 85.16, "maxJumpCount": 4, "shieldStrength": 372.98 } @@ -2698,8 +2698,8 @@ "unladenRange": 38.04, "fullTankRange": 30.11, "ladenRange": 23.03, - "unladenTotalRange": 675.7, - "ladenTotalRange": 501.97, + "unladenFastestRange": 675.7, + "ladenFastestRange": 501.97, "maxJumpCount": 20, "shieldStrength": 372.98 } @@ -2916,8 +2916,8 @@ "unladenRange": 18.49, "fullTankRange": 18.12, "ladenRange": 16.39, - "unladenTotalRange": 73.21, - "ladenTotalRange": 66.15, + "unladenFastestRange": 73.21, + "ladenFastestRange": 66.15, "maxJumpCount": 4, "shieldStrength": 833 } @@ -3044,8 +3044,8 @@ "unladenRange": 35.99, "fullTankRange": 33.36, "ladenRange": 33.36, - "unladenTotalRange": 232.28, - "ladenTotalRange": 232.28, + "unladenFastestRange": 232.28, + "ladenFastestRange": 232.28, "maxJumpCount": 7, "shieldStrength": 92.25 } @@ -3181,8 +3181,8 @@ "unladenRange": 15.06, "fullTankRange": 14.86, "ladenRange": 14.86, - "unladenTotalRange": 42.5, - "ladenTotalRange": 42.5, + "unladenFastestRange": 42.5, + "ladenFastestRange": 42.5, "maxJumpCount": 3, "shieldStrength": 548.74 } @@ -3335,8 +3335,8 @@ "unladenRange": 12.51, "fullTankRange": 12.38, "ladenRange": 12.38, - "unladenTotalRange": 35.35, - "ladenTotalRange": 35.35, + "unladenFastestRange": 35.35, + "ladenFastestRange": 35.35, "maxJumpCount": 3, "shieldStrength": 760.16 } @@ -3454,8 +3454,8 @@ "unladenRange": 17.12, "fullTankRange": 16.71, "ladenRange": 16.71, - "unladenTotalRange": 42.4, - "ladenTotalRange": 42.4, + "unladenFastestRange": 42.4, + "ladenFastestRange": 42.4, "maxJumpCount": 3, "shieldStrength": 102 } @@ -3612,8 +3612,8 @@ "unladenRange": 8.43, "fullTankRange": 8.09, "ladenRange": 7.25, - "unladenTotalRange": 81.5, - "ladenTotalRange": 72.9, + "unladenFastestRange": 81.5, + "ladenFastestRange": 72.9, "maxJumpCount": 10, "shieldStrength": 299.48 } diff --git a/__tests__/test-import.js b/__tests__/test-import.js new file mode 100644 index 00000000..c83ffecf --- /dev/null +++ b/__tests__/test-import.js @@ -0,0 +1,226 @@ +jest.dontMock('../src/app/stores/Persist'); +jest.dontMock('../src/app/components/TranslatedComponent'); +jest.dontMock('../src/app/components/ModalImport'); + +import React from 'react'; +import ReactDOM from 'react-dom'; +import TU from 'react-testutils-additions'; +import Utils from './testUtils'; +import { getLanguage } from '../src/app/i18n/Language'; + +describe('Import Modal', function() { + + let MockRouter = require('../src/app/Router'); + const Persist = require('../src/app/stores/Persist').default; + const ModalImport = require('../src/app/components/ModalImport').default; + const mockContext = { + language: getLanguage('en'), + sizeRatio: 1, + openMenu: jest.genMockFunction(), + closeMenu: jest.genMockFunction(), + showModal: jest.genMockFunction(), + hideModal: jest.genMockFunction(), + tooltip: jest.genMockFunction(), + termtip: jest.genMockFunction(), + onWindowResize: jest.genMockFunction() + }; + + MockRouter.go = jest.genMockFunction(); + + let modal, render, ContextProvider = Utils.createContextProvider(mockContext); + + /** + * Clear saved builds, and reset React DOM + */ + function reset() { + MockRouter.go.mockClear(); + Persist.deleteAll(); + render = TU.renderIntoDocument(); + modal = TU.findRenderedComponentWithType(render, ModalImport); + } + + /** + * Simulate user import text entry / paste + * @param {string} text Import text / raw data + */ + function pasteText(text) { + let textarea = TU.findRenderedDOMComponentWithTag(render, 'textarea'); + TU.Simulate.change(textarea, { target: { value: text } }); + } + + /** + * Simulate click on Proceed button + */ + function clickProceed() { + let proceedButton = TU.findRenderedDOMComponentWithId(render, 'proceed'); + TU.Simulate.click(proceedButton); + } + + /** + * Simulate click on Import button + */ + function clickImport() { + let importButton = TU.findRenderedDOMComponentWithId(render, 'import'); + TU.Simulate.click(importButton); + } + + describe('Import Backup', function() { + + beforeEach(reset); + + it('imports a valid backup', function() { + let importData = require('./fixtures/valid-backup'); + let importString = JSON.stringify(importData); + + expect(modal.state.importValid).toEqual(false); + expect(modal.state.errorMsg).toEqual(null); + pasteText(importString); + expect(modal.state.importValid).toBe(true); + expect(modal.state.errorMsg).toEqual(null); + expect(modal.state.builds).toEqual(importData.builds); + expect(modal.state.comparisons).toEqual(importData.comparisons); + expect(modal.state.shipDiscount).toEqual(importData.discounts[0]); + expect(modal.state.moduleDiscount).toEqual(importData.discounts[1]); + expect(modal.state.insurance).toBe(importData.insurance.toLowerCase()); + clickProceed(); + expect(modal.state.processed).toBe(true); + expect(modal.state.errorMsg).toEqual(null); + clickImport(); + expect(Persist.getBuilds()).toEqual(importData.builds); + expect(Persist.getComparisons()).toEqual(importData.comparisons); + expect(Persist.getInsurance()).toEqual(importData.insurance.toLowerCase()); + expect(Persist.getShipDiscount()).toEqual(importData.discounts[0]); + expect(Persist.getModuleDiscount()).toEqual(importData.discounts[1]); + }); + + it('imports an old valid backup', function() { + const importData = require('./fixtures/old-valid-export'); + const importStr = JSON.stringify(importData); + + pasteText(importStr); + expect(modal.state.builds).toEqual(importData.builds); + expect(modal.state.importValid).toBe(true); + expect(modal.state.errorMsg).toEqual(null); + clickProceed(); + expect(modal.state.processed).toBeTruthy(); + clickImport(); + expect(Persist.getBuilds()).toEqual(importData.builds); + }); + + it('catches an invalid backup', function() { + const importData = require('./fixtures/valid-backup'); + let invalidImportData = Object.assign({}, importData); + invalidImportData.builds.asp = null; // Remove Asp Miner build used in comparison + + pasteText('"this is not valid"'); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('Must be an object or array!'); + pasteText('{ "builds": "Should not be a string" }'); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('builds must be an object!'); + pasteText(JSON.stringify(importData).replace('anaconda', 'invalid_ship')); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('"invalid_ship" is not a valid Ship Id!'); + pasteText(JSON.stringify(importData).replace('Dream', '')); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('Imperial Clipper build "" must be a string at least 1 character long!'); + pasteText(JSON.stringify(invalidImportData)); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('asp build "Miner" data is missing!'); + }); + }); + + describe('Import Detailed Build', function() { + + beforeEach(reset); + + it('imports a valid v3 build', function() { + const importData = require('./fixtures/anaconda-test-detailed-export-v3'); + pasteText(JSON.stringify(importData)); + + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + clickProceed(); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship'); + }); + + it('catches an invalid build', function() { + const importData = require('./fixtures/anaconda-test-detailed-export-v3'); + pasteText(JSON.stringify(importData).replace('components', 'comps')); + + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual('Anaconda Build "Test My Ship": Invalid data'); + }); + }); + + describe('Import Detaild Builds Array', function() { + + beforeEach(reset); + + it('imports all builds', function() { + const importData = require('./fixtures/valid-detailed-export'); + const expectedBuilds = require('./fixtures/expected-builds'); + + pasteText(JSON.stringify(importData)); + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + clickProceed(); + expect(modal.state.processed).toBeTruthy(); + clickImport(); + + let builds = Persist.getBuilds(); + + for (let s in builds) { + for (let b in builds[s]) { + expect(builds[s][b]).toEqual(expectedBuilds[s][b]); + } + } + }); + }); + + describe('Import E:D Shipyard Builds', function() { + + it('imports a valid builds', function() { + const imports = require('./fixtures/ed-shipyard-import-valid'); + + for (let i = 0; i < imports.length; i++ ) { + reset(); + let fixture = imports[i]; + pasteText(fixture.buildText); + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + clickProceed(); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/' + fixture.shipId + '/' + fixture.buildCode + '?bn=' + encodeURIComponent(fixture.buildName)); + } + }); + + it('catches invalid builds', function() { + const imports = require('./fixtures/ed-shipyard-import-invalid'); + + for (let i = 0; i < imports.length; i++ ) { + reset(); + pasteText(imports[i].buildText); + expect(modal.state.importValid).toBeFalsy(); + expect(modal.state.errorMsg).toEqual(imports[i].errorMsg); + } + }); + }); + + describe('Imports from a Comparison', function() { + + it('imports a valid comparison', function() { + const importBuilds = require('./fixtures/valid-backup').builds; + Persist.deleteAll(); + render = TU.renderIntoDocument(); + modal = TU.findRenderedComponentWithType(render, ModalImport); + + expect(modal.state.processed).toBe(true); + expect(modal.state.errorMsg).toEqual(null); + clickImport(); + expect(Persist.getBuilds()).toEqual(importBuilds); + }); + }); + +}); diff --git a/__tests__/test-persist.js b/__tests__/test-persist.js new file mode 100644 index 00000000..5c5aeb4e --- /dev/null +++ b/__tests__/test-persist.js @@ -0,0 +1,143 @@ +jest.dontMock('../src/app/stores/Persist'); + +import React from 'react'; +import ReactDOM from 'react-dom'; +import TU from 'react-testutils-additions'; + +let origAddEventListener = window.addEventListener; +let storageListener; +let ls = {}; + +// Implment mock localStorage +let localStorage = { + getItem: function(key) { + return ls[key]; + }, + setItem: function(key, value) { + ls[key] = value; + }, + removeItem: function(key) { + delete ls[key]; + }, + clear: function() { + ls = {}; + } +} + +window.addEventListener = function(eventName, listener) { + + if(eventName == 'storage') { + storageListener = listener; // Keep track of latest storage listener + } else { + origAddEventListener.apply(arguments); + } +} + +describe('Persist', function() { + + const Persist = require('../src/app/stores/Persist').Persist; + + describe('Multi tab/window', function() { + it("syncs builds", function() { + window.localStorage = localStorage; + ls = {}; + let p = new Persist(); + let newBuilds = { + anaconda: { test: '1234' } + }; + + storageListener({ key: 'builds', newValue: JSON.stringify(newBuilds) }); + expect(p.getBuild('anaconda', 'test')).toBe('1234'); + }); + }); + + describe('General and Settings', function() { + it("has defaults", function() { + window.localStorage = localStorage; + ls = {}; + let p = new Persist(); + expect(p.getLangCode()).toBe('en'); + expect(p.showTooltips()).toBe(true); + expect(p.getInsurance()).toBe('standard'); + expect(p.getShipDiscount()).toBe(0); + expect(p.getModuleDiscount()).toBe(0); + expect(p.getSizeRatio()).toBe(1); + }); + + it("loads from localStorage correctly", function() { + window.localStorage = localStorage; + let savedData = require('./fixtures/valid-backup'); + ls = {}; + ls.builds = JSON.stringify(savedData.builds); + ls.NG_TRANSLATE_LANG_KEY = 'de'; + ls.insurance = 'Standard'; + ls.shipDiscount = 0.25; + ls.moduleDiscount = 0.15; + let p = new Persist(); + + expect(p.getInsurance()).toBe('standard'); + expect(p.getShipDiscount()).toBe(0.25); + expect(p.getModuleDiscount()).toBe(0.15); + expect(p.getLangCode()).toEqual('de'); + expect(p.getBuilds('anaconda')).toEqual(savedData.builds.anaconda); + expect(p.getBuilds('python')).toEqual(savedData.builds.python); + expect(p.getBuildsNamesFor('imperial_clipper')).toEqual(['Cargo', 'Current', 'Dream', 'Multi-purpose']); + expect(p.getBuild('type_7_transport', 'Cargo')).toEqual('02A5D5A4D3D3D5C--------0505040403480101'); + }); + + it("uses defaults from a corrupted localStorage", function() { + window.localStorage = localStorage; + ls = {}; + ls.builds = "not valid json"; + ls.comparisons = "1, 3, 4"; + ls.insurance = 'this insurance does not exist'; + ls.shipDiscount = 'this is not a number'; + ls.moduleDiscount = 10; // Way to big + + let p = new Persist(); + expect(p.getLangCode()).toBe('en'); + expect(p.showTooltips()).toBe(true); + expect(p.getInsurance()).toBe('standard'); + expect(p.getShipDiscount()).toBe(0); + expect(p.getModuleDiscount()).toBe(0); + expect(p.getBuilds()).toEqual({}); + expect(p.getComparisons()).toEqual({}); + }); + + it("works without localStorage", function() { + window.localStorage = null; + let p = new Persist(); + expect(p.getLangCode()).toBe('en'); + expect(p.showTooltips()).toBe(true); + expect(p.getInsurance()).toBe('standard'); + expect(p.getShipDiscount()).toBe(0); + expect(p.getModuleDiscount()).toBe(0); + expect(p.getSizeRatio()).toBe(1); + + p.saveBuild('anaconda', 'test', '12345'); + expect(p.getBuild('anaconda', 'test')).toBe('12345'); + + p.deleteBuild('anaconda', 'test'); + expect(p.hasBuilds()).toBe(false); + }); + + it("generates the backup", function() { + window.localStorage = localStorage; + let savedData = require('./fixtures/valid-backup'); + ls = {}; + ls.builds = JSON.stringify(savedData.builds); + ls.insurance = 'Beta'; + ls.shipDiscount = 0.25; + ls.moduleDiscount = 0.15; + + let p = new Persist(); + let backup = p.getAll(); + + expect(backup.insurance).toBe('beta'); + expect(backup.shipDiscount).toBe(0.25); + expect(backup.moduleDiscount).toBe(0.15); + expect(backup.builds).toEqual(savedData.builds); + expect(backup.comparisons).toEqual({}); + }); + }); +}) diff --git a/__tests__/test-serializer.js b/__tests__/test-serializer.js new file mode 100644 index 00000000..c03aff75 --- /dev/null +++ b/__tests__/test-serializer.js @@ -0,0 +1,63 @@ +import Ship from '../src/app/shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import * as Serializer from '../src/app/shipyard/Serializer'; +import jsen from 'jsen'; + +describe("Serializer", function() { + const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v3'); + const code = anacondaTestExport.references[0].code; + const anaconda = Ships.anaconda; + const validate = jsen(require('../src/schemas/ship-loadout/3')); + + describe("To Detailed Build", function() { + let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code); + let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild); + + it("conforms to the v3 ship-loadout schema", function() { + expect(validate(exportData)).toBe(true); + }); + + it("contains the correct components and stats", function() { + expect(exportData.components).toEqual(anacondaTestExport.components); + expect(exportData.stats).toEqual(anacondaTestExport.stats); + expect(exportData.ship).toEqual(anacondaTestExport.ship); + expect(exportData.name).toEqual(anacondaTestExport.name); + }); + + }); + + describe("Export Detailed Builds", function() { + const expectedExport = require('./fixtures/valid-detailed-export'); + const builds = require('./fixtures/expected-builds'); + const exportData = Serializer.toDetailedExport(builds); + + it("conforms to the v3 ship-loadout schema", function() { + expect(exportData instanceof Array).toBe(true); + + for (let detailedBuild of exportData) { + expect(validate(detailedBuild)).toBe(true); + } + }); + + }); + + describe("From Detailed Build", function() { + + it("builds the ship correctly", function() { + let testBuildA = new Ship('anaconda', anaconda.properties, anaconda.slots); + testBuildA.buildFrom(code); + let testBuildB = Serializer.fromDetailedBuild(anacondaTestExport); + + for(var p in testBuildB) { + if (p == 'availCS') { + continue; + } + expect(testBuildB[p]).toEqual(testBuildA[p], p + ' does not match'); + } + + }); + + }); + + +}); diff --git a/test/tests/test-factory-ship.js b/__tests__/test-ship.js similarity index 59% rename from test/tests/test-factory-ship.js rename to __tests__/test-ship.js index ac31d633..5b9c6a47 100644 --- a/test/tests/test-factory-ship.js +++ b/__tests__/test-ship.js @@ -1,20 +1,15 @@ -describe("Ship Factory", function() { - - var Ship; - var Components; +import Ship from '../src/app/shipyard/Ship'; +import { Ships } from 'coriolis-data/dist'; +import * as ModuleUtils from '../src/app/shipyard/ModuleUtils'; - beforeEach(module('shipyard')); - beforeEach(inject(['Ship', 'Components', function (_Ship_, _Components_) { - Ship = _Ship_; - Components = _Components_; - }])); +describe("Ship Factory", function() { it("can build all ships", function() { - for (var s in DB.ships) { - var shipData = DB.ships[s]; - var ship = new Ship(s, shipData.properties, shipData.slots); + for (let s in Ships) { + let shipData = Ships[s]; + let ship = new Ship(s, shipData.properties, shipData.slots); - for (p in shipData.properties) { + for (let p in shipData.properties) { expect(ship[p]).toEqual(shipData.properties[p], s + ' property [' + p + '] does not match when built'); } @@ -27,8 +22,8 @@ describe("Ship Factory", function() { expect(ship.unladenRange).toBeGreaterThan(0, s + ' unladenRange'); expect(ship.ladenRange).toBeGreaterThan(0, s + ' ladenRange'); expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity'); - expect(ship.unladenTotalRange).toBeGreaterThan(0, s + ' unladenTotalRange'); - expect(ship.ladenTotalRange).toBeGreaterThan(0, s + ' ladenTotalRange'); + expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange'); + expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange'); expect(ship.shieldStrength).toBeGreaterThan(0, s + ' shieldStrength'); expect(ship.armour).toBeGreaterThan(0, s + ' armour'); expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed'); @@ -37,7 +32,7 @@ describe("Ship Factory", function() { it("resets and rebuilds properly", function() { var id = 'cobra_mk_iii'; - var cobra = DB.ships[id]; + var cobra = Ships[id]; var shipA = new Ship(id, cobra.properties, cobra.slots); var shipB = new Ship(id, cobra.properties, cobra.slots); var testShip = new Ship(id, cobra.properties, cobra.slots); @@ -81,84 +76,84 @@ describe("Ship Factory", function() { it("discounts hull and components properly", function() { var id = 'cobra_mk_iii'; - var cobra = DB.ships[id]; + var cobra = Ships[id]; var testShip = new Ship(id, cobra.properties, cobra.slots); testShip.buildWith(cobra.defaults); var originalHullCost = testShip.hullCost; var originalTotalCost = testShip.totalCost; - var discount = 0.9; + var discount = 0.1; - expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); + expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); testShip.applyDiscounts(discount, discount); // Floating point errors cause miniscule decimal places which are handled in the app by rounding/formatting - expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.floor(originalHullCost * discount), 'Discounted Hull cost does not match'); - expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * discount), 'Discounted Total cost does not match'); + expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.floor(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match'); + expect(Math.floor(testShip.totalCost)).toEqual(Math.floor(originalTotalCost * (1 - discount)), 'Discounted Total cost does not match'); - testShip.applyDiscounts(1, 1); // No discount, 100% of cost + testShip.applyDiscounts(0, 0); // No discount, 100% of cost - expect(testShip.c.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); + expect(testShip.m.discountedCost).toEqual(originalHullCost, 'Hull cost does not match'); expect(testShip.totalCost).toEqual(originalTotalCost, 'Total cost does not match'); - testShip.applyDiscounts(discount, 1); // Only discount hull + testShip.applyDiscounts(discount, 0); // Only discount hull - expect(Math.floor(testShip.c.discountedCost)).toEqual(Math.round(originalHullCost * discount), 'Discounted Hull cost does not match'); - expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.c.discountedCost, 'Total cost does not match'); + expect(Math.floor(testShip.m.discountedCost)).toEqual(Math.round(originalHullCost * (1 - discount)), 'Discounted Hull cost does not match'); + expect(testShip.totalCost).toEqual(originalTotalCost - originalHullCost + testShip.m.discountedCost, 'Total cost does not match'); }); it("enforces a single shield generator", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - expect(anaconda.internal[2].c.grp).toEqual('sg', 'Anaconda default shield generator slot'); + expect(anaconda.internal[2].m.grp).toEqual('sg', 'Anaconda default shield generator slot'); - anaconda.use(anaconda.internal[1], '4j', Components.internal('4j')); // 6E Shield Generator + anaconda.use(anaconda.internal[1], ModuleUtils.internal('4j')); // 6E Shield Generator expect(anaconda.internal[2].c).toEqual(null, 'Anaconda default shield generator slot is empty'); - expect(anaconda.internal[2].id).toEqual(null, 'Anaconda default shield generator slot id is null'); - expect(anaconda.internal[1].id).toEqual('4j', 'Slot 1 should have SG 4j in it'); - expect(anaconda.internal[1].c.grp).toEqual('sg','Slot 1 should have SG 4j in it'); + expect(anaconda.internal[2].m).toEqual(null, 'Anaconda default shield generator slot id is null'); + expect(anaconda.internal[1].m.id).toEqual('4j', 'Slot 1 should have SG 4j in it'); + expect(anaconda.internal[1].m.grp).toEqual('sg','Slot 1 should have SG 4j in it'); }); it("enforces a single shield fuel scoop", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - anaconda.use(anaconda.internal[4], '32', Components.internal('32')); // 4A Fuel Scoop - expect(anaconda.internal[4].c.grp).toEqual('fs', 'Anaconda fuel scoop slot'); + anaconda.use(anaconda.internal[4], ModuleUtils.internal('32')); // 4A Fuel Scoop + expect(anaconda.internal[4].m.grp).toEqual('fs', 'Anaconda fuel scoop slot'); - anaconda.use(anaconda.internal[3], '32', Components.internal('32')); + anaconda.use(anaconda.internal[3], ModuleUtils.internal('32')); expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original fuel scoop slot is empty'); - expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original fuel scoop slot id is null'); - expect(anaconda.internal[3].id).toEqual('32', 'Slot 1 should have FS 32 in it'); - expect(anaconda.internal[3].c.grp).toEqual('fs','Slot 1 should have FS 32 in it'); + expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original fuel scoop slot id is null'); + expect(anaconda.internal[3].m.id).toEqual('32', 'Slot 1 should have FS 32 in it'); + expect(anaconda.internal[3].m.grp).toEqual('fs','Slot 1 should have FS 32 in it'); }); it("enforces a single refinery", function() { var id = 'anaconda'; - var anacondaData = DB.ships[id]; + var anacondaData = Ships[id]; var anaconda = new Ship(id, anacondaData.properties, anacondaData.slots); anaconda.buildWith(anacondaData.defaults); - anaconda.use(anaconda.internal[4], '23', Components.internal('23')); // 4E Refinery - expect(anaconda.internal[4].c.grp).toEqual('rf', 'Anaconda refinery slot'); + anaconda.use(anaconda.internal[4], ModuleUtils.internal('23')); // 4E Refinery + expect(anaconda.internal[4].m.grp).toEqual('rf', 'Anaconda refinery slot'); - anaconda.use(anaconda.internal[3], '23', Components.internal('23')); + anaconda.use(anaconda.internal[3], ModuleUtils.internal('23')); expect(anaconda.internal[4].c).toEqual(null, 'Anaconda original refinery slot is empty'); - expect(anaconda.internal[4].id).toEqual(null, 'Anaconda original refinery slot id is null'); - expect(anaconda.internal[3].id).toEqual('23', 'Slot 1 should have RF 23 in it'); - expect(anaconda.internal[3].c.grp).toEqual('rf','Slot 1 should have RF 23 in it'); + expect(anaconda.internal[4].m).toEqual(null, 'Anaconda original refinery slot id is null'); + expect(anaconda.internal[3].m.id).toEqual('23', 'Slot 1 should have RF 23 in it'); + expect(anaconda.internal[3].m.grp).toEqual('rf','Slot 1 should have RF 23 in it'); }); }); diff --git a/__tests__/testUtils.js b/__tests__/testUtils.js new file mode 100644 index 00000000..662f87e8 --- /dev/null +++ b/__tests__/testUtils.js @@ -0,0 +1,24 @@ +import React from 'react'; + +const TestUtils = { + createContextProvider: function(context) { + var _contextTypes = {}; + + Object.keys(context).forEach(function(key) { + _contextTypes[key] = React.PropTypes.any; + }); + + return React.createClass({ + displayName: 'ContextProvider', + childContextTypes: _contextTypes, + getChildContext() { return context; }, + + render() { + return React.Children.only(this.props.children); + } + }); + } +}; + + +export default TestUtils; \ No newline at end of file diff --git a/app/fonts/orbitron-regular-webfont.eot b/app/fonts/orbitron-regular-webfont.eot deleted file mode 100755 index cc95d224..00000000 Binary files a/app/fonts/orbitron-regular-webfont.eot and /dev/null differ diff --git a/app/fonts/orbitron-regular-webfont.svg b/app/fonts/orbitron-regular-webfont.svg deleted file mode 100755 index af25415b..00000000 --- a/app/fonts/orbitron-regular-webfont.svg +++ /dev/null @@ -1,397 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/fonts/orbitron-regular-webfont.ttf b/app/fonts/orbitron-regular-webfont.ttf deleted file mode 100755 index 6b6daaad..00000000 Binary files a/app/fonts/orbitron-regular-webfont.ttf and /dev/null differ diff --git a/app/fonts/orbitron-regular-webfont.woff b/app/fonts/orbitron-regular-webfont.woff deleted file mode 100755 index 03ac6809..00000000 Binary files a/app/fonts/orbitron-regular-webfont.woff and /dev/null differ diff --git a/app/fonts/orbitron-regular-webfont.woff2 b/app/fonts/orbitron-regular-webfont.woff2 deleted file mode 100755 index bb47de77..00000000 Binary files a/app/fonts/orbitron-regular-webfont.woff2 and /dev/null differ diff --git a/app/icons/bin.svg b/app/icons/bin.svg deleted file mode 100755 index 4b05b645..00000000 --- a/app/icons/bin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/icons/cogs.svg b/app/icons/cogs.svg deleted file mode 100755 index f065a951..00000000 --- a/app/icons/cogs.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/coriolis.svg b/app/icons/coriolis.svg deleted file mode 100755 index 471b720d..00000000 --- a/app/icons/coriolis.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/icons/download.svg b/app/icons/download.svg deleted file mode 100755 index 10bc0a02..00000000 --- a/app/icons/download.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/eddb.svg b/app/icons/eddb.svg deleted file mode 100644 index 81baeb6a..00000000 --- a/app/icons/eddb.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/app/icons/embed.svg b/app/icons/embed.svg deleted file mode 100755 index 35df51e9..00000000 --- a/app/icons/embed.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/icons/equalizer.svg b/app/icons/equalizer.svg deleted file mode 100644 index b1d72f2b..00000000 --- a/app/icons/equalizer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/app/icons/floppy-disk.svg b/app/icons/floppy-disk.svg deleted file mode 100755 index dabe06b3..00000000 --- a/app/icons/floppy-disk.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/fuel.svg b/app/icons/fuel.svg deleted file mode 100755 index 69ff2074..00000000 --- a/app/icons/fuel.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/github-mark.svg b/app/icons/github-mark.svg deleted file mode 100755 index 4aca53bf..00000000 --- a/app/icons/github-mark.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/hammer.svg b/app/icons/hammer.svg deleted file mode 100755 index 3bac473c..00000000 --- a/app/icons/hammer.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/infinite.svg b/app/icons/infinite.svg deleted file mode 100755 index 7eb3a117..00000000 --- a/app/icons/infinite.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/icons/info.svg b/app/icons/info.svg deleted file mode 100755 index 7571aede..00000000 --- a/app/icons/info.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/icons/link.svg b/app/icons/link.svg deleted file mode 100755 index 8b24b301..00000000 --- a/app/icons/link.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/icons/mount-F.svg b/app/icons/mount-F.svg deleted file mode 100755 index 7e5fd98d..00000000 --- a/app/icons/mount-F.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/icons/mount-G.svg b/app/icons/mount-G.svg deleted file mode 100755 index f57ffd33..00000000 --- a/app/icons/mount-G.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/icons/mount-T.svg b/app/icons/mount-T.svg deleted file mode 100755 index bb301efe..00000000 --- a/app/icons/mount-T.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/icons/no-power.svg b/app/icons/no-power.svg deleted file mode 100644 index 00f7a9b5..00000000 --- a/app/icons/no-power.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/notification.svg b/app/icons/notification.svg deleted file mode 100755 index 2c9c22fd..00000000 --- a/app/icons/notification.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/power.svg b/app/icons/power.svg deleted file mode 100644 index 08d19243..00000000 --- a/app/icons/power.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/question.svg b/app/icons/question.svg deleted file mode 100755 index 4eec41ab..00000000 --- a/app/icons/question.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/rocket.svg b/app/icons/rocket.svg deleted file mode 100755 index af2684f1..00000000 --- a/app/icons/rocket.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/spinner11.svg b/app/icons/spinner11.svg deleted file mode 100755 index b6588c71..00000000 --- a/app/icons/spinner11.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/app/icons/station-coriolis.svg b/app/icons/station-coriolis.svg deleted file mode 100644 index 7287b5a2..00000000 --- a/app/icons/station-coriolis.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/icons/station-ocellus.svg b/app/icons/station-ocellus.svg deleted file mode 100644 index 9e037fee..00000000 --- a/app/icons/station-ocellus.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/icons/station-orbis.svg b/app/icons/station-orbis.svg deleted file mode 100644 index 1acf2a16..00000000 --- a/app/icons/station-orbis.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/icons/station-outpost.svg b/app/icons/station-outpost.svg deleted file mode 100644 index bd9309f3..00000000 --- a/app/icons/station-outpost.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/icons/stats-bars.svg b/app/icons/stats-bars.svg deleted file mode 100755 index 5ff267a5..00000000 --- a/app/icons/stats-bars.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/switch.svg b/app/icons/switch.svg deleted file mode 100755 index a3949c0d..00000000 --- a/app/icons/switch.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/icons/upload.svg b/app/icons/upload.svg deleted file mode 100755 index 45e3ae6c..00000000 --- a/app/icons/upload.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/icons/warning.svg b/app/icons/warning.svg deleted file mode 100755 index d7e2c9f8..00000000 --- a/app/icons/warning.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/images/logo/browserconfig.xml b/app/images/logo/browserconfig.xml deleted file mode 100755 index 103ee1fd..00000000 --- a/app/images/logo/browserconfig.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - #000000 - - - diff --git a/app/index.html b/app/index.html deleted file mode 100755 index 4dd2af92..00000000 --- a/app/index.html +++ /dev/null @@ -1,87 +0,0 @@ - - - - Coriolis - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
<%= svgContent %>
- -
- -
- - - - - - <% if (uaTracking) { %> - - <% } %> - - - \ No newline at end of file diff --git a/app/js/app.js b/app/js/app.js deleted file mode 100755 index 7b9441b5..00000000 --- a/app/js/app.js +++ /dev/null @@ -1,114 +0,0 @@ -angular.module('app', ['ui.router', 'ct.ui.router.extras.sticky', 'ui.sortable', 'shipyard', 'ngLodash', 'app.templates', 'pascalprecht.translate']) -.run(['$rootScope', '$location', '$window', '$document', '$state', '$translate', 'localeFormat', 'Persist', 'Discounts', 'Languages', 'SizeMap', -function($rootScope, $location, $window, $doc, $state, $translate, localeFormat, Persist, Discounts, Languages, SizeMap) { - // App is running as a standalone web app on tablet/mobile - var isStandAlone; - // This was causing issues on Windows phones ($window.external was causing Angular js to throw an exception). Backup is to try this and set isStandAlone to false if this fails. - try { - isStandAlone = $window.navigator.standalone || ($window.external && $window.external.msIsSiteMode && $window.external.msIsSiteMode()); - } catch (ex) { - isStandAlone = false; - } - - // Redirect any state transition errors to the error controller/state - $rootScope.$on('$stateChangeError', function(e, toState, toParams, fromState, fromParams, error) { - e.preventDefault(); - $state.go('error', error, { location: false, reload: true }); // Go to error state, reload the controller, keep the current URL - }); - - // Track on Google analytics if available - $rootScope.$on('$stateChangeSuccess', function(e, to, toParams, from, fromParams) { - $rootScope.prevState = { name: from.name, params: fromParams }; - - if (to.url) { // Only track states that have a URL - if ($window.ga) { - ga('send', 'pageview', { page: $location.path() }); - } - - if (isStandAlone) { - // Persist the current state - Persist.setState({ name: to.name, params: toParams }); - } - } - }); - - $rootScope.language = { - opts: Languages, - current: Languages[Persist.getLangCode()] ? Persist.getLangCode() : 'en' - }; - $rootScope.localeFormat = d3.locale(localeFormat.get($rootScope.language.current)); - updateNumberFormat(); - - // Global Reference variables - $rootScope.insurance = { opts: [{ name: 'standard', pct: 0.05 }, { name: 'alpha', pct: 0.025 }, { name: 'beta', pct: 0.0375 }] }; - $rootScope.discounts = { opts: Discounts }; - $rootScope.sizeRatio = Persist.getSizeRatio(); - $rootScope.SZM = SizeMap; - $rootScope.title = 'Coriolis'; - - $rootScope.changeLanguage = function() { - $translate.use($rootScope.language.current); - $rootScope.localeFormat = d3.locale(localeFormat.get($rootScope.language.current)); - updateNumberFormat(); - $rootScope.$broadcast('languageChanged', $rootScope.language.current); - }; - - // Formatters - $rootScope.fRPct = d3.format('%'); - $rootScope.fTime = function(d) { return Math.floor(d / 60) + ':' + ('00' + Math.floor(d % 60)).substr(-2, 2); }; - - function updateNumberFormat() { - var locale = $rootScope.localeFormat; - var fGen = $rootScope.fGen = locale.numberFormat('n'); - $rootScope.fCrd = locale.numberFormat(',.0f'); - $rootScope.fPwr = locale.numberFormat(',.2f'); - $rootScope.fRound = function(d) { return fGen(d3.round(d, 2)); }; - $rootScope.fPct = locale.numberFormat('.2%'); - $rootScope.f1Pct = locale.numberFormat('.1%'); - } - - /** - * Returns the name of the component mounted in the specified slot - * @param {Object} slot The slot object - * @return {String} The component name - */ - $rootScope.cName = function(slot) { - return $translate.instant(slot.c ? slot.c.name ? slot.c.name : slot.c.grp : null); - }; - - // Global Event Listeners - $doc.bind('keyup', function(e) { - if (e.keyCode == 27) { // Escape Key - $rootScope.$broadcast('close', e); - $rootScope.$apply(); - } else { - $rootScope.$broadcast('keyup', e); - } - }); - - $rootScope.bgClicked = function(e) { - $rootScope.$broadcast('close', e); - }; - - if ($window.applicationCache) { - // Listen for appcache updated event, present refresh to update view - $window.applicationCache.addEventListener('updateready', function() { - if ($window.applicationCache.status == $window.applicationCache.UPDATEREADY) { - // Browser downloaded a new app cache. - $rootScope.appCacheUpdate = true; - $rootScope.$apply(); - } - }, false); - } - - if (isStandAlone) { - var state = Persist.getState(); - // If a previous state has been stored, load that state - if (state && state.name && state.params) { - $state.go(state.name, state.params, { location: 'replace' }); - } else { - $state.go('shipyard', null, { location: 'replace' }); // Default to home page - } - } - -}]); diff --git a/app/js/config.js b/app/js/config.js deleted file mode 100755 index 243e6e8d..00000000 --- a/app/js/config.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Sets up the routes and handlers before the Angular app is kicked off. - */ -angular.module('app').config(['$provide', '$stateProvider', '$urlRouterProvider', '$locationProvider', 'ShipsDB', function($provide, $stateProvider, $urlRouterProvider, $locationProvider, ships) { - // Use HTML5 push and replace state if possible - $locationProvider.html5Mode({ enabled: true, requireBase: false }); - - /** - * Set up all states and their routes. - */ - $stateProvider - .state('outfit', { - url: '/outfit/:shipId/:code?bn', - params: { - shipId: { value: 'sidewinder', squash: false }, // Allow 'shipId' parameter to default to sidewinder - code: { value: null, squash: true } // Allow 'code' parameter to be empty/optional - }, - templateUrl: 'views/page-outfit.html', - controller: 'OutfitController', - resolve: { - shipId: ['$stateParams', function($p) { // Ensure ship exists before loading controller - if (!ships[$p.shipId]) { - throw { type: 'no-ship', message: $p.shipId }; - } - }] - }, - sticky: true - }) - .state('compare', { - url: '/compare/:name', - params: { - name: { value: null, squash: true } - }, - templateUrl: 'views/page-comparison.html', - controller: 'ComparisonController', - sticky: true - }) - .state('comparison', { - url: '/comparison/:code', - templateUrl: 'views/page-comparison.html', - controller: 'ComparisonController', - sticky: true - }) - .state('shipyard', { url: '/', templateUrl: 'views/page-shipyard.html', controller: 'ShipyardController', sticky: true }) - .state('error', { params: { type: null, message: null, details: null }, templateUrl: 'views/page-error.html', controller: 'ErrorController', sticky: true }) - - // Modal States and views - .state('modal', { abstract: true, views: { 'modal': { templateUrl: 'views/_modal.html', controller: 'ModalController' } } }) - .state('modal.about', { views: { 'modal-content': { templateUrl: 'views/modal-about.html' } } }) - .state('modal.export', { params: { title: null, data: null, promise: null, description: null }, views: { 'modal-content': { templateUrl: 'views/modal-export.html', controller: 'ExportController' } } }) - .state('modal.import', { params: { obj: null }, views: { 'modal-content': { templateUrl: 'views/modal-import.html', controller: 'ImportController' } } }) - .state('modal.link', { params: { url: null }, views: { 'modal-content': { templateUrl: 'views/modal-link.html', controller: 'LinkController' } } }) - .state('modal.delete', { views: { 'modal-content': { templateUrl: 'views/modal-delete.html', controller: 'DeleteController' } } }); - - - // Redirects - $urlRouterProvider.when('/outfit', '/outfit/sidewinder'); - - /** - * 404 Handler - Keep current URL/ do not redirect, change to error state. - */ - $urlRouterProvider.otherwise(function($injector, $location) { - // Go to error state, reload the controller, keep the current URL - $injector.get('$state').go('error', { type: 404, message: null, details: null }, { location: false, reload: true }); - return $location.path; - }); - - /** - * Global Error Handler. Decorates the existing error handler such that it - * redirects uncaught errors to the error page. - * - */ - $provide.decorator('$exceptionHandler', ['$delegate', '$injector', function($delegate, $injector) { - return function(err, cause) { - // Go to error state, reload the controller, keep the current URL - $injector.get('$state').go('error', { type: null, message: err.message, details: err.stack }, { location: false, reload: true }); - $delegate(err, cause); - }; - }]); - -}]); diff --git a/app/js/controllers/controller-comparison.js b/app/js/controllers/controller-comparison.js deleted file mode 100755 index e1c4b9ff..00000000 --- a/app/js/controllers/controller-comparison.js +++ /dev/null @@ -1,243 +0,0 @@ -angular.module('app').controller('ComparisonController', ['lodash', '$rootScope', '$filter', '$scope', '$state', '$stateParams', '$translate', 'Utils', 'ShipFacets', 'ShipsDB', 'Ship', 'Persist', 'Serializer', function(_, $rootScope, $filter, $scope, $state, $stateParams, $translate, Utils, ShipFacets, Ships, Ship, Persist, Serializer) { - $rootScope.title = 'Coriolis - Compare'; - $scope.predicate = 'name'; // Sort by ship name as default - $scope.desc = false; - $scope.facetSortOpts = { containment: '#facet-container', orderChanged: function() { $scope.saved = false; } }; - $scope.builds = []; - $scope.unusedBuilds = []; - $scope.name = $stateParams.name; - $scope.compareMode = !$stateParams.code; - $scope.importObj = {}; // Used for importing comparison builds (from permalinked comparison) - var defaultFacets = [9, 6, 4, 1, 3, 2]; // Reverse order of Armour, Shields, Speed, Jump Range, Cargo Capacity, Cost - var facets = $scope.facets = angular.copy(ShipFacets); - var shipId, buildName, comparisonData; - - /** - * Add an existing build to the comparison. The build must be saved locally. - * @param {string} id The unique ship key/id - * @param {string} name The build name - */ - $scope.addBuild = function(id, name, code) { - var data = Ships[id]; // Get ship properties - code = code ? code : Persist.builds[id][name]; // Retrieve build code if not passed - - if (!code) { // No build found - return; - } - - var b = new Ship(id, data.properties, data.slots); // Create a new Ship instance - Serializer.toShip(b, code); // Populate components from code - // Extend ship instance and add properties below - b.buildName = name; - b.code = code; - b.pctRetracted = b.powerRetracted / b.powerAvailable; - b.pctDeployed = b.powerDeployed / b.powerAvailable; - $scope.builds.push(b); // Add ship build to comparison - $scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc); // Resort - _.remove($scope.unusedBuilds, function(o) { // Remove from unused builds - return o.id == id && o.buildName == name; - }); - $scope.saved = false; - }; - - /** - * Removes a build from the comparison - * @param {string} id The unique ship key/id - * @param {string} name The build name - */ - $scope.removeBuild = function(id, name) { - _.remove($scope.builds, function(s) { - if (s.id == id && s.buildName == name) { - $scope.unusedBuilds.push({ id: id, buildName: name, name: s.name }); // Add build back to unused builds - return true; - } - return false; - }); - $scope.saved = false; - }; - - /** - * Toggles the selected the set of facets used in the comparison - * @param {number} i The index of the facet in facets - */ - $scope.toggleFacet = function(i) { - facets[i].active = !facets[i].active; - $scope.tblUpdate = !$scope.tblUpdate; // Simple switch to trigger the table to update - $scope.saved = false; - }; - - /** - * Click handler for sorting by facets in the table - * @param {object} e Event object - */ - $scope.handleClick = function(e) { - var elem = angular.element(e.target); - if (elem.attr('prop')) { // Get component ID - $scope.sort(elem.attr('prop')); - - } else if (elem.attr('del')) { // Delete index - $scope.removeBuild(elem.attr('del')); - } - }; - - /** - * Sort the comparison array based on the selected facet / ship property - * @param {string} key Ship property - */ - $scope.sort = function(key) { - $scope.desc = $scope.predicate == key ? !$scope.desc : $scope.desc; - $scope.predicate = key; - $scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc); - }; - - /** - * Saves the current comparison's selected facets and builds - */ - $scope.save = function() { - $scope.name = $scope.name.trim(); - if ($scope.name == 'all') { - return; - } - var selectedFacets = []; - facets.forEach(function(f) { - if (f.active) { - selectedFacets.unshift(f.index); - } - }); - Persist.saveComparison($scope.name, $scope.builds, selectedFacets); - $state.go('compare', { name: $scope.name }, { location: 'replace', notify: false }); - $scope.saved = true; - }; - - /** - * Permantently delete the current comparison - */ - $scope.delete = function() { - Persist.deleteComparison($scope.name); - $state.go('compare', { name: null }, { location: 'replace', reload: true }); - }; - - /** - * Set saved to false when the name of the comparison is changed. - */ - $scope.nameChange = function() { - $scope.saved = false; - }; - - /** - * Hide/Show the select builds menu - * @param {boolean} s Show true/false - * @param {Event} e Event Object - */ - $scope.selectBuilds = function(s, e) { - e.stopPropagation(); - $scope.showBuilds = s; - }; - - /** - * Show the permalink modal - * @param {Event} e Event object - */ - $scope.permalink = function(e) { - e.stopPropagation(); - $state.go('modal.link', { url: genPermalink() }); - }; - - /** - * Generate the forum embed code for the comparison - * and show the export modal. - * - * @param {Event} e Event object - */ - $scope.embed = function(e) { - e.stopPropagation(); - // Make a request to goo.gl to shorten the URL, returns a promise - var promise = Utils.shortenUrl( genPermalink()).then( - function(shortUrl) { - return Utils.comparisonBBCode(facets, $scope.builds, shortUrl); - }, - function(err) { - return 'Error - ' + err.statusText; - } - ); - $state.go('modal.export', { promise: promise, title: $translate.instant('FORUM') + ' BBCode' }); - }; - - /** - * Generates the long permalink URL - * @return {string} The long permalink URL - */ - function genPermalink() { - var selectedFacets = []; - facets.forEach(function(f) { - if (f.active) { - selectedFacets.unshift(f.index); - } - }); - var code = Serializer.fromComparison( - $scope.name, - $scope.builds, - selectedFacets, - $scope.predicate, - $scope.desc - ); - return $state.href('comparison', { code: code }, { absolute: true }); - } - - /* Event listeners */ - $scope.$on('close', function() { - $scope.showBuilds = false; - }); - - $scope.$on('languageChanged', function() { - $scope.tblUpdate = !$scope.tblUpdate; // Simple switch to trigger the table to update - }); - - /* Initialization */ - if ($scope.compareMode) { - if ($scope.name == 'all') { - for (shipId in Persist.builds) { - for (buildName in Persist.builds[shipId]) { - $scope.addBuild(shipId, buildName); - } - } - } else { - for (shipId in Persist.builds) { - for (buildName in Persist.builds[shipId]) { - $scope.unusedBuilds.push({ id: shipId, buildName: buildName, name: Ships[shipId].properties.name }); - } - } - comparisonData = Persist.getComparison($scope.name); - if (comparisonData) { - defaultFacets = comparisonData.facets; - comparisonData.builds.forEach(function(b) { - $scope.addBuild(b.shipId, b.buildName); - }); - $scope.saved = true; - } - } - } else { - try { - comparisonData = Serializer.toComparison($stateParams.code); - defaultFacets = comparisonData.f; - $scope.name = comparisonData.n; - $scope.predicate = comparisonData.p; - $scope.desc = comparisonData.d; - comparisonData.b.forEach(function(build) { - $scope.addBuild(build.s, build.n, build.c); - if (!$scope.importObj[build.s]) { - $scope.importObj[build.s] = {}; - } - $scope.importObj[build.s][build.n] = build.c; - }); - } catch (e) { - throw { type: 'bad-comparison', message: e.message, details: e }; - } - } - // Replace fmt with actual format function as defined in rootScope and retain original index - facets.forEach(function(f, i) { f.index = i; }); - // Remove default facets, mark as active, and add them back in selected order - _.pullAt(facets, defaultFacets).forEach(function(f) { f.active = true; facets.unshift(f); }); - $scope.builds = $filter('orderBy')($scope.builds, $scope.predicate, $scope.desc); - -}]); diff --git a/app/js/controllers/controller-delete.js b/app/js/controllers/controller-delete.js deleted file mode 100755 index 5aec906e..00000000 --- a/app/js/controllers/controller-delete.js +++ /dev/null @@ -1,7 +0,0 @@ -angular.module('app').controller('DeleteController', ['$scope', 'Persist', function($scope, Persist) { - $scope.deleteAll = function() { - Persist.deleteAll(); - $scope.$parent.dismiss(); - }; - -}]); diff --git a/app/js/controllers/controller-error.js b/app/js/controllers/controller-error.js deleted file mode 100755 index b1f19cf1..00000000 --- a/app/js/controllers/controller-error.js +++ /dev/null @@ -1,29 +0,0 @@ -angular.module('app') -.controller('ErrorController', ['$window', '$rootScope', '$scope', '$stateParams', '$location', function($window, $rootScope, $scope, $p, $location) { - $rootScope.title = 'Error'; - $scope.path = $location.path(); - $scope.type = $p.type || 'unknown'; - $scope.browser = $window.navigator.userAgent; - - switch ($scope.type) { - case 404: - $scope.msgPre = 'Page'; - $scope.msgHighlight = $scope.path; - $scope.msgPost = 'Not Found'; - break; - case 'no-ship': - $scope.msgPre = 'Ship'; - $scope.msgHighlight = $p.message; - $scope.msgPost = 'does not exist'; - break; - case 'build-fail': - $scope.msgPre = 'Build Failure!'; - $scope.details = $p.details; - break; - default: - $scope.msgPre = 'Uh, Jameson, we have a problem..'; - $scope.errorMessage = $p.message; - $scope.details = $p.details; - } - -}]); diff --git a/app/js/controllers/controller-export.js b/app/js/controllers/controller-export.js deleted file mode 100755 index d2ade80c..00000000 --- a/app/js/controllers/controller-export.js +++ /dev/null @@ -1,19 +0,0 @@ -angular.module('app').controller('ExportController', ['$scope', '$stateParams', function($scope, $stateParams) { - - $scope.title = $stateParams.title || 'Export'; - $scope.description = $stateParams.description; - - if ($stateParams.promise) { - $scope.export = 'Generating...'; - $stateParams.promise.then(function(data) { - $scope.export = (typeof data === 'object') ? angular.toJson(data, true) : data; - }); - } else { - $scope.export = angular.toJson($stateParams.data, true); - } - - $scope.onTextClick = function($event) { - $event.target.select(); - }; - -}]); diff --git a/app/js/controllers/controller-import.js b/app/js/controllers/controller-import.js deleted file mode 100755 index 10ced797..00000000 --- a/app/js/controllers/controller-import.js +++ /dev/null @@ -1,316 +0,0 @@ -angular.module('app').controller('ImportController', ['lodash', '$rootScope', '$scope', '$stateParams', 'ShipsDB', 'Ship', 'Components', 'GroupMap', 'Persist', 'Serializer', function(_, $rootScope, $scope, $stateParams, Ships, Ship, Components, GroupMap, Persist, Serializer) { - $scope.importValid = false; - $scope.importString = null; - $scope.errorMsg = null; - $scope.canEdit = true; - $scope.builds = $stateParams.obj || null; - $scope.ships = Ships; - - var textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n'); - var lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)'); - var mountMap = { 'H': 4, 'L': 3, 'M': 2, 'S': 1, 'U': 0 }; - var standardMap = { 'RB': 0, 'TM': 1, 'FH': 2, 'EC': 3, 'PC': 4, 'SS': 5, 'FS': 6 }; - var bhMap = { 'lightweight alloy': 0, 'reinforced alloy': 1, 'military grade composite': 2, 'mirrored surface composite': 3, 'reactive surface composite': 4 }; - - function isEmptySlot(slot) { - return slot.maxClass == this && slot.c === null; - } - - function equalsIgnoreCase(str) { - return str.toLowerCase() == this.toLowerCase(); - } - - function validateBuild(shipId, code, name) { - var shipData = Ships[shipId]; - - if (!shipData) { - throw '"' + shipId + '" is not a valid Ship Id!'; - } - if (typeof name != 'string' || name.length == 0) { - throw shipData.properties.name + ' build "' + name + '" must be a string at least 1 character long!'; - } - if (typeof code != 'string' || code.length < 10) { - throw shipData.properties.name + ' build "' + name + '" is not valid!'; - } - try { - Serializer.toShip(new Ship(shipId, shipData.properties, shipData.slots), code); - } catch (e) { - throw shipData.properties.name + ' build "' + name + '" is not valid!'; - } - } - - function detailedJsonToBuild(detailedBuild) { - var ship; - if (!detailedBuild.name) { - throw 'Build Name missing!'; - } - - if (!detailedBuild.name.trim()) { - throw 'Build Name must be a string at least 1 character long!'; - } - - try { - ship = Serializer.fromDetailedBuild(detailedBuild); - } catch (e) { - throw detailedBuild.ship + ' Build "' + detailedBuild.name + '": Invalid data'; - } - - return { shipId: ship.id, name: detailedBuild.name, code: Serializer.fromShip(ship) }; - } - - function importBackup(importData) { - if (importData.builds && typeof importData.builds == 'object') { - for (var shipId in importData.builds) { - for (var buildName in importData.builds[shipId]) { - validateBuild(shipId, importData.builds[shipId][buildName], buildName); - } - } - $scope.builds = importData.builds; - } else { - throw 'builds must be an object!'; - } - if (importData.comparisons) { - for (var compName in importData.comparisons) { - var comparison = importData.comparisons[compName]; - for (var i = 0, l = comparison.builds.length; i < l; i++) { - var build = comparison.builds[i]; - if (!importData.builds[build.shipId] || !importData.builds[build.shipId][build.buildName]) { - throw build.shipId + ' build "' + build.buildName + '" data is missing!'; - } - } - } - $scope.comparisons = importData.comparisons; - } - if (importData.discounts instanceof Array && importData.discounts.length == 2) { - $scope.discounts = importData.discounts; - } - if (typeof importData.insurance == 'string' && importData.insurance.length > 3) { - $scope.insurance = importData.insurance; - } - } - - function importDetailedArray(importArr) { - var builds = {}; - for (var i = 0, l = importArr.length; i < l; i++) { - var build = detailedJsonToBuild(importArr[i]); - if (!builds[build.shipId]) { - builds[build.shipId] = {}; - } - builds[build.shipId][build.name] = build.code; - } - $scope.builds = builds; - } - - function importTextBuild(buildStr) { - var buildName = textBuildRegex.exec(buildStr)[1].trim(); - var shipName = buildName.toLowerCase(); - var shipId = null; - - for (var sId in Ships) { - if (Ships[sId].properties.name.toLowerCase() == shipName) { - shipId = sId; - break; - } - } - - if (!shipId) { throw 'No such ship found: "' + buildName + '"'; } - - var lines = buildStr.split('\n'); - var ship = new Ship(shipId, Ships[shipId].properties, Ships[shipId].slots); - ship.buildWith(null); - - for (var i = 1; i < lines.length; i++) { - var line = lines[i].trim(); - - if (!line) { continue; } - if (line.substring(0, 3) == '---') { break; } - - var parts = lineRegex.exec(line); - - if (!parts) { throw 'Error parsing: "' + line + '"'; } - - var typeSize = parts[1]; - var cl = parts[2]; - var rating = parts[3]; - var mount = parts[4]; - var missile = parts[5]; - var name = parts[6].trim(); - var slot, group; - - if (isNaN(typeSize)) { // Standard or Hardpoint - if (typeSize.length == 1) { // Hardpoint - var slotClass = mountMap[typeSize]; - - if (cl > slotClass) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; } - - slot = _.find(ship.hardpoints, isEmptySlot, slotClass); - - if (!slot) { throw 'No hardpoint slot available for: "' + line + '"'; } - - group = _.find(GroupMap, equalsIgnoreCase, name); - - var hp = Components.findHardpoint(group, cl, rating, group ? null : name, mount, missile); - - if (!hp) { throw 'Unknown component: "' + line + '"'; } - - ship.use(slot, hp.id, hp, true); - - } else if (typeSize == 'BH') { - var bhId = bhMap[name.toLowerCase()]; - - if (bhId === undefined) { throw 'Unknown bulkhead: "' + line + '"'; } - - ship.useBulkhead(bhId, true); - - } else if (standardMap[typeSize] != undefined) { - var standardIndex = standardMap[typeSize]; - - if (ship.standard[standardIndex].maxClass < cl) { throw name + ' exceeds max class for the ' + ship.name; } - - ship.use(ship.standard[standardIndex], cl + rating, Components.standard(standardIndex, cl + rating), true); - - } else { - throw 'Unknown component: "' + line + '"'; - } - } else { - if (cl > typeSize) { throw cl + rating + ' ' + name + ' exceeds slot size: "' + line + '"'; } - - slot = _.find(ship.internal, isEmptySlot, typeSize); - - if (!slot) { throw 'No internal slot available for: "' + line + '"'; } - - group = _.find(GroupMap, equalsIgnoreCase, name); - - var intComp = Components.findInternal(group, cl, rating, group ? null : name); - - if (!intComp) { throw 'Unknown component: "' + line + '"'; } - - ship.use(slot, intComp.id, intComp); - } - } - - var builds = {}; - builds[shipId] = {}; - builds[shipId]['Imported ' + buildName] = Serializer.fromShip(ship); - $scope.builds = builds; - } - - $scope.validateImport = function() { - var importData = null; - var importString = $scope.importString.trim(); - $scope.importValid = false; - $scope.errorMsg = null; - $scope.builds = $scope.discounts = $scope.comparisons = $scope.insurance = null; - - if (!importString) { return; } - - - try { - if (textBuildRegex.test(importString)) { // E:D Shipyard build text - importTextBuild(importString); - } else { // JSON Build data - importData = angular.fromJson($scope.importString); - - if (!importData || typeof importData != 'object') { - throw 'Must be an object or array!'; - } - - if (importData instanceof Array) { // Must be detailed export json - importDetailedArray(importData); - } else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export - importDetailedArray([importData]); // Convert to array with singleobject - } else { // Using Backup JSON - importBackup(importData); - } - } - } catch (e) { - $scope.errorMsg = (typeof e == 'string') ? e : 'Cannot Parse the data!'; - return; - } - - $scope.importValid = true; - }; - - $scope.hasBuild = function(shipId, name) { - return Persist.getBuild(shipId, name) !== null; - }; - - $scope.hasComparison = function(name) { - return Persist.getComparison(name) !== null; - }; - - $scope.process = function() { - if ($scope.builds) { - var builds = $scope.builds; - for (var shipId in builds) { - for (var buildName in builds[shipId]) { - var code = builds[shipId][buildName]; - // Update builds object such that orginal name retained, but can be renamed - builds[shipId][buildName] = { - code: code, - useName: buildName - }; - } - } - } - - if ($scope.comparisons) { - var comparisons = $scope.comparisons; - for (var name in comparisons) { - comparisons[name].useName = name; - } - } - - $scope.processed = true; - }; - - $scope.import = function() { - - if ($scope.builds) { - var builds = $scope.builds; - for (var shipId in builds) { - for (var buildName in builds[shipId]) { - var build = builds[shipId][buildName]; - var name = build.useName.trim(); - if (name) { - Persist.saveBuild(shipId, name, build.code); - } - } - } - } - - if ($scope.comparisons) { - var comparisons = $scope.comparisons; - for (var comp in comparisons) { - var comparison = comparisons[comp]; - var useName = comparison.useName.trim(); - if (useName) { - Persist.saveComparison(useName, comparison.builds, comparison.facets); - } - } - } - - if ($scope.discounts) { - $rootScope.discounts.ship = $scope.discounts[0]; - $rootScope.discounts.components = $scope.discounts[1]; - $rootScope.$broadcast('discountChange'); - Persist.setDiscount($scope.discounts); - } - - if ($scope.insurance) { - $rootScope.insurance.current = $scope.insurance; - Persist.setInsurance($scope.insurance); - } - - $scope.$parent.dismiss(); - }; - - /* Initialization */ - - if ($scope.builds) { // If import is passed an build object - $scope.canEdit = false; - $scope.process(); - } - - -}]); diff --git a/app/js/controllers/controller-link.js b/app/js/controllers/controller-link.js deleted file mode 100755 index 59c4690c..00000000 --- a/app/js/controllers/controller-link.js +++ /dev/null @@ -1,16 +0,0 @@ -angular.module('app').controller('LinkController', ['$scope', 'Utils', '$stateParams', function($scope, Utils, $stateParams) { - $scope.url = $stateParams.url; - $scope.shortenedUrl = 'Shortening...'; - - $scope.onTextClick = function($event) { - $event.target.select(); - }; - - Utils.shortenUrl($scope.url) - .then(function(url) { - $scope.shortenedUrl = url; - }, function(e) { - $scope.shortenedUrl = 'Error - ' + e.statusText; - }); - -}]); diff --git a/app/js/controllers/controller-modal.js b/app/js/controllers/controller-modal.js deleted file mode 100755 index c73135ad..00000000 --- a/app/js/controllers/controller-modal.js +++ /dev/null @@ -1,14 +0,0 @@ -angular.module('app').controller('ModalController', ['$rootScope', '$scope', '$state', function($rootScope, $scope, $state) { - - $scope.dismiss = function() { - if ($rootScope.prevState) { - var state = $rootScope.prevState; - $state.go(state.name, state.params, { location: 'replace', reload: false }); - } else { - $state.go('shipyard'); - } - }; - - $scope.$on('close', $scope.dismiss); - -}]); diff --git a/app/js/controllers/controller-outfit.js b/app/js/controllers/controller-outfit.js deleted file mode 100755 index 02aa05cf..00000000 --- a/app/js/controllers/controller-outfit.js +++ /dev/null @@ -1,655 +0,0 @@ -angular.module('app').controller('OutfitController', ['$window', '$rootScope', '$scope', '$state', '$stateParams', '$translate', 'ShipsDB', 'Ship', 'Components', 'Serializer', 'Persist', 'calcTotalRange', 'calcSpeed', function($window, $rootScope, $scope, $state, $p, $translate, Ships, Ship, Components, Serializer, Persist, calcTotalRange, calcSpeed) { - var win = angular.element($window); // Angularized window object for event triggering - var data = Ships[$p.shipId]; // Retrieve the basic ship properties, slots and defaults - var ship = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship instance - var retrofitShip = new Ship($p.shipId, data.properties, data.slots); // Create a new Ship for retrofit comparison - - // Update the ship instance with the code (if provided) or the 'factory' defaults. - if ($p.code) { - Serializer.toShip(ship, $p.code); // Populate components from 'code' URL param - $scope.code = $p.code; - } else { - ship.buildWith(data.defaults); // Populate with default components - } - - $scope.buildName = $p.bn ? $window.decodeURIComponent($p.bn) : null; - $scope.ships = Ships; - $rootScope.title = ship.name + ($scope.buildName ? ' - ' + $scope.buildName : ''); - $scope.ship = ship; - $scope.pp = ship.standard[0]; // Power Plant - $scope.th = ship.standard[1]; // Thruster - $scope.fsd = ship.standard[2]; // Frame Shrift Drive - $scope.ls = ship.standard[3]; // Life Support - $scope.pd = ship.standard[4]; // Power Distributor - $scope.ss = ship.standard[5]; // Sensors - $scope.ft = ship.standard[6]; // Fuel Tank - $scope.hps = ship.hardpoints; - $scope.internal = ship.internal; - $scope.costList = ship.costList; - $scope.powerList = ship.powerList; - $scope.priorityBands = ship.priorityBands; - $scope.availCS = ship.getAvailableComponents(); - $scope.selectedSlot = null; - $scope.savedCode = Persist.getBuild(ship.id, $scope.buildName); - $scope.canSave = Persist.isEnabled(); - $scope.allBuilds = Persist.builds; - $scope.fuel = 0; - $scope.pwrDesc = false; - $scope.pwrPredicate = 'type'; - $scope.retroDesc = false; - $scope.retroPredicate = 'netCost'; - $scope.costDesc = true; - $scope.costPredicate = 'c.cost'; - $scope.ammoDesc = true; - $scope.ammoPredicate = 'ammoUnitCost'; - $scope.costTab = Persist.getCostTab() || 'costs'; - - if ($scope.savedCode) { - Serializer.toShip(retrofitShip, $scope.savedCode); // Populate components from last save - $scope.retrofitBuild = $scope.buildName; - } else { - retrofitShip.buildWith(data.defaults); - $scope.retrofitBuild = null; - } - - ship.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components); - retrofitShip.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components); - updateRetrofitCosts(); - - $scope.jrSeries = { - xMin: 0, - xMax: ship.cargoCapacity, - yMax: ship.unladenRange, - yMin: 0, - func: function(cargo) { // X Axis is Cargo - return ship.getJumpRangeForMass(ship.unladenMass + $scope.fuel + cargo, $scope.fuel); - } - }; - $scope.jrChart = { - labels: { - xAxis: { - title: 'cargo', - unit: 'T' - }, - yAxis: { - title: 'jump range', - unit: 'LY' - } - } - }; - - $scope.trSeries = { - xMin: 0, - xMax: ship.cargoCapacity, - yMax: ship.unladenTotalRange, - yMin: 0, - func: function(cargo) { // X Axis is Cargo - return calcTotalRange(ship.unladenMass + cargo, $scope.fsd.c, $scope.fuel); - } - }; - $scope.trChart = { - labels: { - xAxis: { - title: 'cargo', - unit: 'T' - }, - yAxis: { - title: 'total range', - unit: 'LY' - } - } - }; - - $scope.speedSeries = { - xMin: 0, - xMax: ship.cargoCapacity, - yMax: calcSpeed(ship.unladenMass, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed).boost, - yMin: 0, - series: ['boost', '4 Pips', '2 Pips', '0 Pips'], - colors: ['#0088d2', '#ff8c0d', '#D26D00', '#c06400'], - func: function(cargo) { // X Axis is Cargo - return calcSpeed(ship.unladenMass + $scope.fuel + cargo, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed); - } - }; - $scope.speedChart = { - labels: { - xAxis: { - title: 'cargo', - unit: 'T' - }, - yAxis: { - title: 'speed', - unit: 'm/s' - } - } - }; - - /** - * 'Opens' a select for component selection. - * - * @param {[type]} e The event object - * @param {[type]} slot The slot that is being 'opened' for selection - */ - $scope.selectSlot = function(e, slot) { - e.stopPropagation(); - if ($scope.selectedSlot == slot) { - $scope.selectedSlot = null; - } else { - $scope.selectedSlot = slot; - } - }; - - /** - * Updates the ships build with the selected component for the - * specified slot. Prevents the click event from propagation. - * - * @param {string} type Shorthand key/string for identifying the slot & component type - * @param {[type]} slot The slot object belonging to the ship instance - * @param {[type]} e The event object - */ - $scope.select = function(type, slot, e, id) { - e.stopPropagation(); - - if (!id) { // Find component id if not passed - var elem = e.target; - while (elem && elem !== e.currentTarget && !elem.getAttribute('cpid')) { - elem = elem.parentElement; - } - if (elem) { - id = elem.getAttribute('cpid'); - } - } - - if (id) { - if (id == 'empty') { - ship.use(slot, null, null); - } else if (type == 'h') { - ship.use(slot, id, Components.hardpoints(id)); - } else if (type == 'c') { - ship.use(slot, id, Components.standard(ship.standard.indexOf(slot), id)); - } else if (type == 'i') { - ship.use(slot, id, Components.internal(id)); - } else if (type == 'b') { - ship.useBulkhead(id); - } - $scope.selectedSlot = null; - updateState(Serializer.fromShip(ship)); - } - }; - - /** - * Reload the build from the last save. - */ - $scope.reloadBuild = function() { - if ($scope.buildName && $scope.savedCode) { - Serializer.toShip(ship, $scope.savedCode); // Repopulate with components from last save - updateState($scope.savedCode); - } - }; - - $scope.resetBuild = function() { - ship.buildWith(data.defaults); // Populate with default components - updateState(null); - }; - - /** - * Optimize for the lower mass build that can still boost and power the ship - * without power management. - */ - $scope.optimizeMassBuild = function() { - updateState(Serializer.fromShip(ship.optimizeMass())); - }; - - /** - * Optimize for the lower mass build that can still boost and power the ship - * without power management. - */ - $scope.optimizeStandard = function() { - updateState(Serializer.fromShip(ship.useLightestStandard())); - }; - - $scope.useStandard = function(rating) { - updateState(Serializer.fromShip(ship.useStandard(rating))); - }; - - $scope.useHardpoint = function(group, mount, clobber, missile) { - updateState(Serializer.fromShip(ship.useWeapon(group, mount, clobber, missile))); - }; - - $scope.useUtility = function(group, rating, clobber) { - updateState(Serializer.fromShip(ship.useUtility(group, rating, clobber))); - }; - - $scope.emptyInternal = function() { - updateState(Serializer.fromShip(ship.emptyInternal())); - }; - - $scope.emptyHardpoints = function() { - updateState(Serializer.fromShip(ship.emptyWeapons())); - }; - - $scope.emptyUtility = function() { - updateState(Serializer.fromShip(ship.emptyUtility())); - }; - - $scope.fillWithCargo = function() { - ship.internal.forEach(function(slot) { - var id = Components.findInternalId('cr', slot.maxClass, 'E'); - if (!slot.c) { - ship.use(slot, id, Components.internal(id)); - } - }); - updateState(Serializer.fromShip(ship)); - }; - - $scope.fillWithCells = function() { - var chargeCap = 0; // Capacity of single activation - ship.internal.forEach(function(slot) { - var id = Components.findInternalId('scb', slot.maxClass, 'A'); - if (!slot.c && (!slot.eligible || slot.eligible.scb)) { // Check eligibility because of Orca, don't overwrite generator - ship.use(slot, id, Components.internal(id)); - chargeCap += Components.internal(id).recharge; - ship.setSlotEnabled(slot, chargeCap <= ship.shieldStrength); // Don't waste cell capacity on overcharge - } - }); - updateState(Serializer.fromShip(ship)); - }; - - $scope.fillWithArmor = function() { - ship.internal.forEach(function(slot) { - var hr = Components.findInternal('hr', Math.min(slot.maxClass, 5), 'D'); // Hull reinforcements top out at 5D - if (!slot.c && hr) { - ship.use(slot, hr.id, hr); - } - }); - updateState(Serializer.fromShip(ship)); - }; - - /** - * Fill all internal slots with Cargo Racks, and optmize internal components. - * Hardpoints are not altered. - */ - $scope.optimizeCargo = function() { - ship.internal.forEach(function(slot) { - var id = Components.findInternalId('cr', slot.maxClass, 'E'); - ship.use(slot, id, Components.internal(id)); - }); - ship.useLightestStandard(); - updateState(Serializer.fromShip(ship)); - }; - - /** - * Optimize standard and internal components, hardpoints for exploration - */ - $scope.optimizeExplorer = function() { - var intLength = ship.internal.length, - heatSinkCount = 2, // Fit 2 heat sinks if possible - afmUnitCount = 2, // Fit 2 AFM Units if possible - sgSlot, - fuelScoopSlot, - sgId = $scope.availCS.lightestShieldGenerator(ship.hullMass), - sg = Components.internal(sgId); - - ship.setSlotEnabled(ship.cargoHatch, false) - .use(ship.internal[--intLength], '2f', Components.internal('2f')) // Advanced Discovery Scanner - .use(ship.internal[--intLength], '2i', Components.internal('2i')); // Detailed Surface Scanner - - for (var i = 0; i < intLength; i++) { - var slot = ship.internal[i]; - var nextSlot = (i + 1) < intLength ? ship.internal[i + 1] : null; - if (!fuelScoopSlot && (!slot.eligible || slot.eligible.fs)) { // Fit best possible Fuel Scoop - var fuelScoopId = Components.findInternalId('fs', slot.maxClass, 'A'); - fuelScoopSlot = slot; - ship.use(fuelScoopSlot, fuelScoopId, Components.internal(fuelScoopId)); - ship.setSlotEnabled(fuelScoopSlot, true); - - // Mount a Shield generator if possible AND an AFM Unit has been mounted already (Guarantees at least 1 AFM Unit) - } else if (!sgSlot && afmUnitCount < 2 && sg.class <= slot.maxClass && (!slot.eligible || slot.eligible.sg) && (!nextSlot || nextSlot.maxClass < sg.class)) { - sgSlot = slot; - ship.use(sgSlot, sgId, sg); - ship.setSlotEnabled(sgSlot, true); - } else if (afmUnitCount > 0 && (!slot.eligible || slot.eligible.am)) { - afmUnitCount--; - var am = Components.findInternal('am', slot.maxClass, afmUnitCount ? 'B' : 'A'); - ship.use(slot, am.id, am); - ship.setSlotEnabled(slot, false); // Disabled power for AFM Unit - - } else { - ship.use(slot, null, null); - } - } - - ship.hardpoints.forEach(function(s) { - if (s.maxClass == 0 && heatSinkCount) { // Mount up to 2 heatsinks - ship.use(s, '02', Components.hardpoints('02')); - ship.setSlotEnabled(s, heatSinkCount == 2); // Only enable a single Heatsink - heatSinkCount--; - } else { - ship.use(s, null, null); - } - }); - - if (sgSlot) { - // The SG and Fuel scoop to not need to be powered at the same time - if (sgSlot.c.power > fuelScoopSlot.c.power) { // The Shield generator uses the most power - ship.setSlotEnabled(fuelScoopSlot, false); - } else { // The Fuel scoop uses the most power - ship.setSlotEnabled(sgSlot, false); - } - } - - ship.useLightestStandard({ pd: '1D', ppRating: 'A' }); - updateState(Serializer.fromShip(ship)); - }; - - /** - * Save the current build. Will replace the saved build if there is one - * for this ship & with the exact name. - */ - $scope.saveBuild = function() { - if (!$scope.buildName) { - return; - } - // No change hav been made, i.e. save ship default build under a name - if (!$scope.code) { - $scope.code = Serializer.fromShip(ship); - } - // Only save if there a build name and a change has been made or the build has never been saved - if ($scope.code != $scope.savedCode) { - Persist.saveBuild(ship.id, $scope.buildName, $scope.code); - $scope.savedCode = $scope.code; - if ($scope.retrofitBuild === $scope.buildName) { - Serializer.toShip(retrofitShip, $scope.code); - } - updateState($scope.code); - } - }; - - /** - * Export the build to detailed JSON - */ - $scope.exportBuild = function(e) { - e.stopPropagation(); - - if ($scope.buildName) { - $state.go('modal.export', { - title: $scope.buildName + ' ' + $translate.instant('export'), - description: $translate.instant('PHRASE_EXPORT_DESC'), - data: Serializer.toDetailedBuild($scope.buildName, ship, $scope.code || Serializer.fromShip(ship)) - }); - } - }; - - /** - * Permanently delete the current build and redirect/reload this controller - * with the 'factory' build of the current ship. - */ - $scope.deleteBuild = function() { - Persist.deleteBuild(ship.id, $scope.buildName); - $state.go('outfit', { shipId: ship.id, code: null, bn: null }, { location: 'replace', reload: true }); - }; - - /** - * On build name change, retrieve the existing saved code if there is one - */ - $scope.bnChange = function() { - $scope.savedCode = Persist.getBuild(ship.id, $scope.buildName); - }; - - /** - * Toggle cost of the selected component - * @param {object} item The component being toggled - */ - $scope.toggleCost = function(item) { - ship.setCostIncluded(item, !item.incCost); - }; - -/** - * Toggle cost of the selected component for retrofitting comparison - * @param {object} item The component being toggled - */ - $scope.toggleRetrofitCost = function(item) { - retrofitShip.setCostIncluded(item, !item.incCost); - updateRetrofitCosts(); - }; - - /** - * [sortCost description] - * @param {[type]} key [description] - * @return {[type]} [description] - */ - $scope.sortCost = function(key) { - $scope.costDesc = $scope.costPredicate == key ? !$scope.costDesc : $scope.costDesc; - $scope.costPredicate = key; - }; - - $scope.sortPwr = function(key) { - $scope.pwrDesc = $scope.pwrPredicate == key ? !$scope.pwrDesc : $scope.pwrDesc; - $scope.pwrPredicate = key; - }; - - $scope.sortRetrofit = function(key) { - $scope.retroDesc = $scope.retroPredicate == key ? !$scope.retroDesc : $scope.retroDesc; - $scope.retroPredicate = key; - }; - - $scope.sortAmmo = function(key) { - $scope.ammoDesc = $scope.ammoPredicate == key ? !$scope.ammoDesc : $scope.ammoDesc; - $scope.ammoPredicate = key; - }; - - /** - * Toggle the power on/off for the selected component - * @param {object} item The component being toggled - */ - $scope.togglePwr = function(c) { - ship.setSlotEnabled(c, !c.enabled); - updateState(Serializer.fromShip(ship)); - }; - - $scope.incPriority = function(c) { - if (ship.changePriority(c, c.priority + 1)) { - updateState(Serializer.fromShip(ship)); - } - }; - - $scope.decPriority = function(c) { - if (ship.changePriority(c, c.priority - 1)) { - updateState(Serializer.fromShip(ship)); - } - }; - - $scope.fuelChange = function(fuel) { - $scope.fuel = fuel; - updateAmmoCosts(); - win.triggerHandler('render'); - }; - - $scope.statusRetracted = function(slot) { - return ship.getSlotStatus(slot, false); - }; - - $scope.statusDeployed = function(slot) { - return ship.getSlotStatus(slot, true); - }; - - $scope.setRetrofitBase = function() { - if ($scope.retrofitBuild) { - Serializer.toShip(retrofitShip, Persist.getBuild(ship.id, $scope.retrofitBuild)); - } else { - retrofitShip.buildWith(data.defaults); - } - updateRetrofitCosts(); - }; - - $scope.updateCostTab = function(tab) { - Persist.setCostTab(tab); - $scope.costTab = tab; - }; - - $scope.ppWarning = function(pp) { - return pp.pGen < ship.powerRetracted; - }; - - $scope.pdWarning = function(pd) { - return pd.enginecapacity < ship.boostEnergy; - }; - - // Utilify functions - - function updateState(code) { - $scope.code = code; - $state.go('outfit', { shipId: ship.id, code: $scope.code, bn: $scope.buildName }, { location: 'replace', notify: false }); - $scope.speedSeries.xMax = $scope.trSeries.xMax = $scope.jrSeries.xMax = ship.cargoCapacity; - $scope.jrSeries.yMax = ship.unladenRange; - $scope.trSeries.yMax = ship.unladenTotalRange; - $scope.speedSeries.yMax = calcSpeed(ship.unladenMass, ship.speed, ship.boost, $scope.th.c, ship.pipSpeed).boost; - updateRetrofitCosts(); - win.triggerHandler('pwrchange'); - } - - function updateRetrofitCosts() { - var costs = $scope.retrofitList = []; - var total = 0, i, l, item; - - if (ship.bulkheads.id != retrofitShip.bulkheads.id) { - item = { - buyClassRating: ship.bulkheads.c.class + ship.bulkheads.c.rating, - buyName: ship.bulkheads.c.name, - sellClassRating: retrofitShip.bulkheads.c.class + retrofitShip.bulkheads.c.rating, - sellName: retrofitShip.bulkheads.c.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].c.name || slotGroup[i].c.grp; - item.buyClassRating = slotGroup[i].c.class + slotGroup[i].c.rating; - item.netCost = slotGroup[i].discountedCost; - } - if (retroSlotGroup[i].id) { - item.sellName = retroSlotGroup[i].c.name || retroSlotGroup[i].c.grp; - item.sellClassRating = retroSlotGroup[i].c.class + retroSlotGroup[i].c.rating; - item.netCost -= retroSlotGroup[i].discountedCost; - } - costs.push(item); - if (retroSlotGroup[i].incCost) { - total += item.netCost; - } - } - } - } - $scope.retrofitTotal = total; - updateAmmoCosts(); - } - - function updateAmmoCosts() { - var costs = $scope.ammoList = []; - var total = 0, i, l, item, q, limpets = 0, srvs = 0, scoop = false; - - for (var g in { standard: 1, internal: 1, hardpoints: 1 }) { - var slotGroup = ship[g]; - for (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].c.grp) { - case 'fs': //skip fuel calculation if scoop present - scoop = true; - break; - case 'scb': - q = slotGroup[i].c.cells; - break; - case 'am': - q = slotGroup[i].c.ammo; - break; - case 'fx': case 'hb': case 'cc': case 'pc': - limpets = ship.cargoCapacity; - break; - case 'pv': - srvs += slotGroup[i].c.bays; - break; - default: - q = slotGroup[i].c.clip + slotGroup[i].c.ammo; - } - //calculate ammo costs only if a cost is specified - if (slotGroup[i].c.ammocost > 0) { - item = { - ammoClassRating: slotGroup[i].c.class + slotGroup[i].c.rating, - ammoName: slotGroup[i].c.name || slotGroup[i].c.grp, - ammoMax: q, - ammoUnitCost: slotGroup[i].c.ammocost, - ammoTotalCost: q * slotGroup[i].c.ammocost - }; - costs.push(item); - total += item.ammoTotalCost; - } - } - } - } - - //limpets if controllers exist and cargo space available - if (srvs > 0) { - item = { - ammoName: 'SRVs', - ammoMax: srvs, - ammoUnitCost: 6005, - ammoTotalCost: srvs * 6005 - }; - costs.push(item); - total += item.ammoTotalCost; - } - //limpets if controllers exist and cargo space available - if (limpets > 0) { - item = { - ammoName: 'limpets', - ammoMax: ship.cargoCapacity, - ammoUnitCost: 101, - ammoTotalCost: ship.cargoCapacity * 101 - }; - costs.push(item); - total += item.ammoTotalCost; - } - //calculate refuel costs if no scoop present - if (!scoop) { - item = { - ammoName: 'fuel', - ammoMax: $scope.fuel, - ammoUnitCost: 50, - ammoTotalCost: $scope.fuel * 50 - }; - costs.push(item); - total += item.ammoTotalCost; - } - $scope.ammoTotal = total; - } - - // Hide any open menu/slot/etc if the background is clicked - $scope.$on('close', function() { - $scope.selectedSlot = null; - }); - - // Hide any open menu/slot/etc if the background is clicked - $scope.$on('languageChanged', function() { - $scope.selectedSlot = null; - }); - - // Hide any open menu/slot/etc if the background is clicked - $scope.$on('discountChange', function() { - ship.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components); - retrofitShip.applyDiscounts($rootScope.discounts.ship, $rootScope.discounts.components); - updateRetrofitCosts(); - }); - -}]); diff --git a/app/js/controllers/controller-shipyard.js b/app/js/controllers/controller-shipyard.js deleted file mode 100755 index 3ef910a4..00000000 --- a/app/js/controllers/controller-shipyard.js +++ /dev/null @@ -1,59 +0,0 @@ - angular.module('app').controller('ShipyardController', ['$rootScope', '$scope', 'ShipsDB', 'Ship', 'Components', function($rootScope, $scope, ShipsDB, Ship, Components) { - $rootScope.title = 'Coriolis - Shipyard'; - $scope.shipPredicate = 'properties.name'; - $scope.shipDesc = false; - - function countHp(slot) { - this.hp[slot.maxClass]++; - this.hpCount++; - } - - function countInt(slot) { - var 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 ? Components.findInternal('cr', slot.maxClass, 'E').capacity : 0; - } - - function shipSummary(shipId, shipData) { - var summary = angular.copy(shipData.properties); - var ship = new Ship(shipId, shipData.properties, shipData.slots); - summary.id = s; - summary.hpCount = 0; - summary.intCount = 0; - summary.maxCargo = 0; - summary.hp = [0, 0, 0, 0, 0]; // Utility, Small, Medium, Large, Huge - summary.int = [0, 0, 0, 0, 0, 0, 0, 0]; // Sizes 1 - 8 - // 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 - ship.optimizeMass({ th: ship.standard[1].maxClass + 'A' }); // Optmize mass with Max Thrusters - summary.topSpeed = ship.topSpeed; - summary.topBoost = ship.topBoost; - - return summary; - } - - /* Initialization */ - - if (!$rootScope.shipsOverview) { // Only generate this once - $rootScope.shipsOverview = []; - for (var s in ShipsDB) { - $scope.shipsOverview.push(shipSummary(s, ShipsDB[s])); - } - } - - /** - * Sort ships - * @param {object} key Sort predicate - */ - $scope.sortShips = function(key) { - $scope.shipDesc = $scope.shipPredicate == key ? !$scope.shipDesc : $scope.shipDesc; - $scope.shipPredicate = key; - }; - -}]); diff --git a/app/js/directives/directive-area-chart.js b/app/js/directives/directive-area-chart.js deleted file mode 100755 index 2d78d41e..00000000 --- a/app/js/directives/directive-area-chart.js +++ /dev/null @@ -1,164 +0,0 @@ -angular.module('app').directive('areaChart', ['$window', '$translate', function($window, $translate) { - return { - restrict: 'A', - scope: { - config: '=', - series: '=' - }, - link: function(scope, element) { - var series = scope.series, - config = scope.config, - labels = config.labels, - margin = { top: 15, right: 15, bottom: 35, left: 60 }, - fmt = d3.format('.3r'), - fmtLong = d3.format('.2f'), - func = series.func, - drag = d3.behavior.drag(), - dragging = false, - // Define Axes - xAxis = d3.svg.axis().outerTickSize(0).orient('bottom').tickFormat(d3.format('.2r')), - yAxis = d3.svg.axis().ticks(6).outerTickSize(0).orient('left').tickFormat(fmt), - x = d3.scale.linear(), - y = d3.scale.linear(), - data = []; - - // Create chart - var svg = d3.select(element[0]).append('svg'); - var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - // Define Area - var area = d3.svg.area(); - - var gradient = vis.append('defs') - .append('linearGradient') - .attr('id', 'gradient') - .attr('x1', '0%').attr('y1', '0%') - .attr('x2', '100%').attr('y2', '100%') - .attr('spreadMethod', 'pad'); - gradient.append('stop') - .attr('offset', '0%') - .attr('stop-color', '#ff8c0d') - .attr('stop-opacity', 1); - gradient.append('stop') - .attr('offset', '100%') - .attr('stop-color', '#ff3b00') - .attr('stop-opacity', 1); - - // Create Y Axis SVG Elements - var yTxt = vis.append('g').attr('class', 'y axis') - .append('text') - .attr('class', 'cap') - .attr('transform', 'rotate(-90)') - .attr('y', -50) - .attr('dy', '.1em') - .style('text-anchor', 'middle') - .text($translate.instant(labels.yAxis.title) + ' (' + $translate.instant(labels.yAxis.unit) + ')'); - // Create X Axis SVG Elements - var xLbl = vis.append('g').attr('class', 'x axis'); - var xTxt = xLbl.append('text') - .attr('y', 30) - .attr('dy', '.1em') - .style('text-anchor', 'middle') - .text($translate.instant(labels.xAxis.title) + ' (' + $translate.instant(labels.xAxis.unit) + ')'); - - // Create and Add tooltip - var tip = vis.append('g').style('display', 'none'); - tip.append('rect').attr('width', '4.5em').attr('height', '2em').attr('x', '0.5em').attr('y', '-1em').attr('class', 'tip'); - tip.append('circle') - .attr('class', 'marker') - .attr('r', 4); - tip.append('text').attr('class', 'label x').attr('y', '-0.25em'); - tip.append('text').attr('class', 'label y').attr('y', '0.85em'); - - vis.insert('path', ':first-child') // Area/Path to appear behind everything else - .data([data]) - .attr('class', 'area') - .attr('fill', 'url(#gradient)') - .attr('d', area) - .on('mouseover', showTip) - .on('mouseout', hideTip) - .on('mousemove', moveTip) - .call(drag); - - drag - .on('dragstart', function() { - dragging = true; - moveTip.call(this); - showTip(); - }) - .on('dragend', function() { - dragging = false; - hideTip(); - }) - .on('drag', moveTip); - - /** - * Watch for changes in the series data (mass changes, etc) - */ - scope.$watchCollection('series', render); - angular.element($window).bind('orientationchange resize render', render); - - function render() { - var width = element[0].parentElement.offsetWidth, - height = width * 0.5, - w = width - margin.left - margin.right, - h = height - margin.top - margin.bottom; - - data.length = 0; // Reset Data array - - if (series.xMax == series.xMin) { - var yVal = func(series.xMin); - data.push([ series.xMin, yVal ]); - data.push([ series.xMin, yVal ]); - area.x(function(d, i) { return i * w; }).y0(h).y1(function(d) { return y(d[1]); }); - } else { - for (var val = series.xMin; val <= series.xMax; val += 1) { - data.push([ val, func(val) ]); - } - area.x(function(d) { return x(d[0]); }).y0(h).y1(function(d) { return y(d[1]); }); - } - - // Update Chart Size - svg.attr('width', width).attr('height', height); - // Update domain and scale for axes - x.range([0, w]).domain([series.xMin, series.xMax]).clamp(true); - xAxis.scale(x); - xLbl.attr('transform', 'translate(0,' + h + ')'); - xTxt.attr('x', w / 2); - y.range([h, 0]).domain([series.yMin, series.yMax]); - yAxis.scale(y); - yTxt.attr('x', -h / 2); - vis.selectAll('.y.axis').call(yAxis); - vis.selectAll('.x.axis').call(xAxis); - - vis.selectAll('path.area') // Area/Path to appear behind everything else - .data([data]) - .attr('d', area); - } - - function showTip() { - tip.style('display', null); - } - - function hideTip() { - if (!dragging) { - tip.style('display', 'none'); - } - } - - function moveTip() { - var xPos = d3.mouse(this)[0], x0 = x.invert(xPos), y0 = func(x0), flip = (x0 / x.domain()[1] > 0.65); - tip.attr('transform', 'translate(' + x(x0) + ',' + y(y0) + ')'); - tip.selectAll('rect').attr('x', flip ? '-5.75em' : '0.5em').style('text-anchor', flip ? 'end' : 'start'); - tip.selectAll('text.label').attr('x', flip ? '-2em' : '1em').style('text-anchor', flip ? 'end' : 'start'); - tip.select('text.label.x').text(fmtLong(x0) + ' ' + $translate.instant(labels.xAxis.unit)); - tip.select('text.label.y').text(fmtLong(y0) + ' ' + $translate.instant(labels.yAxis.unit)); - } - - scope.$on('$destroy', function() { - angular.element($window).unbind('orientationchange resize render', render); - }); - - } - }; -}]); diff --git a/app/js/directives/directive-bar-chart.js b/app/js/directives/directive-bar-chart.js deleted file mode 100755 index 55e6018e..00000000 --- a/app/js/directives/directive-bar-chart.js +++ /dev/null @@ -1,134 +0,0 @@ -angular.module('app').directive('barChart', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) { - - function bName(build) { - return build.buildName + '\n' + build.name; - } - - function insertLinebreaks(d) { - var el = d3.select(this); - var lines = d.split('\n'); - el.text('').attr('y', -6); - for (var i = 0; i < lines.length; i++) { - var tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]); - if (i > 0) { - tspan.attr('x', -9).attr('dy', '1em'); - } else { - tspan.attr('class', 'primary'); - } - } - } - - return { - restrict: 'A', - scope: { - data: '=', - facet: '=' - }, - link: function(scope, element) { - var color = d3.scale.ordinal().range([ '#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c']), - labels = scope.facet.lbls, - fmt = null, - unit = null, - properties = scope.facet.props, - margin = { top: 10, right: 20, bottom: 40, left: 150 }, - y0 = d3.scale.ordinal(), - y1 = d3.scale.ordinal(), - x = d3.scale.linear(), - yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left'), - xAxis = d3.svg.axis().scale(x).ticks(5).outerTickSize(0).orient('bottom'); - - // Create chart - var svg = d3.select(element[0]).append('svg'); - var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - - // Create and Add tooltip - var tip = d3.tip() - .attr('class', 'd3-tip') - .html(function(property, propertyIndex) { - return (labels ? ($translate.instant(labels[propertyIndex]) + ': ') : '') + fmt(property.value) + ' ' + unit; - }); - - vis.call(tip); - - // Create Y Axis SVG Elements - vis.append('g').attr('class', 'y axis'); - vis.selectAll('g.y.axis g text').each(insertLinebreaks); - // Create X Axis SVG Elements - var xAxisLbl = vis.append('g') - .attr('class', 'x axis cap') - .append('text') - .attr('y', 33) - .attr('dy', '.1em') - .style('text-anchor', 'middle'); - - updateFormats(); - - function render() { - var data = scope.data, - width = element[0].offsetWidth, - w = width - margin.left - margin.right, - height = 50 + (30 * data.length * $rootScope.sizeRatio), - h = height - margin.top - margin.bottom, - maxVal = d3.max(data, function(d) { return d3.max(properties, function(p) {return d[p]; }); }); - - // Update chart size - svg.attr('width', width).attr('height', height); - - // Remove existing elements - vis.selectAll('.ship').remove(); - vis.selectAll('rect').remove(); - - // Update X & Y Axis - x.range([0, w]).domain([0, maxVal]); - y0.domain(data.map(bName)).rangeRoundBands([0, h], 0.3); - y1.domain(properties).rangeRoundBands([0, y0.rangeBand()]); - vis.selectAll('.y.axis').call(yAxis); - vis.selectAll('.x.axis').attr('transform', 'translate(0,' + h + ')').call(xAxis); - xAxisLbl.attr('x', w / 2); - // Update Y-Axis labels - vis.selectAll('g.y.axis g text').each(insertLinebreaks); - - var group = vis.selectAll('.ship') - .data(scope.data, bName) - .enter().append('g') - .attr('class', 'g') - .attr('transform', function(build) { return 'translate(0,' + y0(bName(build)) + ')'; }); - - group.selectAll('rect') - .data(function(build) { - var o = []; - for (var i = 0; i < properties.length; i++) { - o.push({ name: properties[i], value: build[properties[i]] }); - } - return o; - }) - .enter().append('rect') - .attr('height', y1.rangeBand()) - .attr('x', 0) - .attr('y', function(d) {return y1(d.name); }) - .attr('width', function(d) { return x(d.value); }) - .on('mouseover', tip.show) - .on('mouseout', tip.hide) - .style('fill', function(d) { return color(d.name); }); - - } - - function updateFormats() { - fmt = $rootScope[scope.facet.fmt]; - unit = $translate.instant(scope.facet.unit); - xAxisLbl.text($translate.instant(scope.facet.title) + (unit ? (' (' + $translate.instant(unit) + ')') : '')); - xAxis.tickFormat($rootScope.localeFormat.numberFormat('.2s')); - render(); - } - - angular.element($window).bind('orientationchange resize render', render); - scope.$watchCollection('data', render); // Watch for changes in the comparison array - scope.$on('languageChanged', updateFormats); - scope.$on('$destroy', function() { - angular.element($window).unbind('orientationchange resize render', render); - tip.destroy(); // Remove the tooltip from the DOM - }); - - } - }; -}]); diff --git a/app/js/directives/directive-comparison-table.js b/app/js/directives/directive-comparison-table.js deleted file mode 100755 index 731f9d16..00000000 --- a/app/js/directives/directive-comparison-table.js +++ /dev/null @@ -1,80 +0,0 @@ -angular.module('app').directive('comparisonTable', ['$state', '$translate', '$rootScope', function($state, $translate, $rootScope) { - - function tblHeader(facets) { - var r1 = ['', $translate.instant('SHIP'), '', $translate.instant('BUILD'), '']; - var r2 = []; - for (var i = 0, l = facets.length; i < l; i++) { - if (facets[i].active) { - var f = facets[i]; - var p = f.props; - var pl = p.length; - r1.push('', $translate.instant(f.lbls[j]), ''); - } - } - - r1.push('>', $translate.instant(f.title), ''); - } - } - r1.push(''); - r1.push(r2.join('')); - r1.push(''); - return r1.join(''); - } - - function tblBody(facets, builds) { - var body = []; - - if (builds.length === 0) { - return 'No builds added to comparison!'); - var href = $state.href('outfit', { shipId: b.id, code: b.code, bn: b.buildName }); - body.push('', b.name, ''); - body.push('', b.buildName, ''); - - for (var j = 0, fl = facets.length; j < fl; j++) { - if (facets[j].active) { - var f = facets[j]; - var p = f.props; - for (var k = 0, pl = p.length; k < pl; k++) { - body.push('', $rootScope[f.fmt](b[p[k]]), ' ', $translate.instant(f.unit), ''); - } - } - } - body.push(''); - } - - return body.join(''); - } - - return { - restrict: 'A', - - link: function(scope, element) { - var header = angular.element(''); - var body = angular.element(''); - element.append(header); - element.append(body); - - var updateAll = function() { - header.html(tblHeader(scope.facets)); - body.html(tblBody(scope.facets, scope.builds)); - }; - - scope.$watchCollection('facets', updateAll); - scope.$watch('tblUpdate', updateAll); - scope.$watchCollection('builds', function() { - body.html(tblBody(scope.facets, scope.builds)); - }); - } - }; -}]); diff --git a/app/js/directives/directive-component-select.js b/app/js/directives/directive-component-select.js deleted file mode 100755 index dba65e9e..00000000 --- a/app/js/directives/directive-component-select.js +++ /dev/null @@ -1,90 +0,0 @@ -angular.module('app').directive('componentSelect', ['$translate', function($translate) { - - // Generting the HTML in this manner is MUCH faster than using an angular template. - - function appendGroup(list, opts, cid, mass, checkWarning) { - var prevClass = null, prevRating = null; - for (var i = 0; i < opts.length; i++) { - var o = opts[i]; - var id = o.id || (o.class + o.rating); // Standard components' ID is their class and rating - - if (i > 0 && opts.length > 3 && o.class != prevClass && (o.rating != prevRating || o.mode) && o.grp != 'pa') { - list.push('
'); - } - - list.push('
  • '); - - if (o.mode) { - list.push(' '); - } - - list.push('', o.class, o.rating); - - if (o.missile) { - list.push('/' + o.missile); - } - - - if (o.name) { - list.push(' ' + $translate.instant(o.name)); - } - - list.push('
  • '); - prevClass = o.class; - prevRating = o.rating; - } - } - - return { - restrict: 'A', - scope: { - opts: '=', // Component Options object - groups: '=', // Groups of Component Options - mass: '=', // Current ship mass - s: '=', // Current Slot - warning: '=' // Check warning function - }, - link: function(scope, element) { - var list = []; - var cid = scope.s.id; // Slot's current component id - var component = scope.s.c; // Slot's Current Component (may be null/undefined) - var opts = scope.opts; - var groups = scope.groups; - var mass = (scope.mass ? scope.mass : 0) - (component && component.mass ? component.mass : 0); // Mass minus the currently selected component - - if (groups) { - // At present time slots with grouped options (Hardpoints and Internal) can be empty - list.push('
    ', $translate.instant('empty'), '
    '); - for (var g in groups) { - var grp = groups[g]; - var grpCode = grp[Object.keys(grp)[0]].grp; // Nasty operation to get the grp property of the first/any single component - list.push('
    ', $translate.instant(g), '
    '); - } - } else { - list.push(''); - } - - element.html(list.join('')); - // If groups are present and a component is already selectd - if (groups && component && component.grp) { - var groupElement = angular.element(document.getElementById(component.grp)); - var parentElem = element[0].parentElement; - parentElem.scrollTop = groupElement[0].offsetTop; // Scroll to currently selected group - } - } - }; -}]); diff --git a/app/js/directives/directive-context-menu.js b/app/js/directives/directive-context-menu.js deleted file mode 100644 index 0004033d..00000000 --- a/app/js/directives/directive-context-menu.js +++ /dev/null @@ -1,14 +0,0 @@ -angular.module('app').directive('contextMenu', ['$parse', function($parse) { - return function(scope, element, attrs) { - var fn = $parse(attrs.contextMenu); - - element.bind('contextmenu', function(e) { - if (!e.shiftKey) { - scope.$apply(function() { - e.preventDefault(); - fn(scope, { $event: e }); - }); - } - }); - }; -}]); diff --git a/app/js/directives/directive-header.js b/app/js/directives/directive-header.js deleted file mode 100755 index eab4601b..00000000 --- a/app/js/directives/directive-header.js +++ /dev/null @@ -1,114 +0,0 @@ -angular.module('app').directive('shipyardHeader', ['lodash', '$window', '$rootScope', '$state', 'Persist', 'Serializer', 'ShipsDB', function(_, $window, $rootScope, $state, Persist, Serializer, ships) { - - - return { - restrict: 'E', - templateUrl: 'views/_header.html', - scope: true, - link: function(scope) { - scope.openedMenu = null; - scope.ships = ships; - scope.allBuilds = Persist.builds; - scope.buildsList = Object.keys(scope.allBuilds).sort(); - scope.allComparisons = Object.keys(Persist.comparisons).sort(); - scope.bs = Persist.state; - - var win = angular.element($window); // Angularized window object for event triggering - var insIndex = _.findIndex($rootScope.insurance.opts, 'name', Persist.getInsurance()); - var savedDiscounts = Persist.getDiscount() || [1, 1]; - $rootScope.insurance.current = $rootScope.insurance.opts[insIndex != -1 ? insIndex : 0]; - $rootScope.discounts.ship = savedDiscounts[0]; - $rootScope.discounts.components = savedDiscounts[1]; - - /** - * Save selected insurance option - */ - scope.updateInsurance = function() { - Persist.setInsurance($rootScope.insurance.current.name); - }; - - /** - * Save selected discount option - */ - scope.updateDiscount = function() { - Persist.setDiscount([$rootScope.discounts.ship, $rootScope.discounts.components]); - $rootScope.$broadcast('discountChange'); - }; - - scope.backup = function(e) { - e.preventDefault(); - e.stopPropagation(); - scope.openedMenu = null; - $state.go('modal.export', { - title: 'backup', - data: Persist.getAll(), - description: 'PHRASE_BACKUP_DESC' - }); - }; - - scope.detailedExport = function(e) { - e.preventDefault(); - e.stopPropagation(); - scope.openedMenu = null; - $state.go('modal.export', { - title: 'detailed export', - data: Serializer.toDetailedExport(scope.allBuilds), - description: 'PHRASE_EXPORT_DESC' - }); - }; - - scope.cleanedBuildList = function(shipId) { - return Object.keys(scope.allBuilds[shipId]); - }; - - scope.openMenu = function(e, menu) { - e.stopPropagation(); - if (menu == scope.openedMenu) { - scope.openedMenu = null; - return; - } - - if ((menu == 'comp' || menu == 'b') && !scope.bs.hasBuilds) { - scope.openedMenu = null; - return; - } - - if (menu == 'comp') { - scope.allComparisons = Object.keys(Persist.comparisons).sort(); - } - - scope.openedMenu = menu; - }; - - // Close menus if a navigation change event occurs - $rootScope.$on('$stateChangeStart', function() { - scope.openedMenu = null; - }); - - // Listen to close event to close opened menus or modals - $rootScope.$on('close', function() { - scope.openedMenu = null; - }); - - scope.textSizeChange = function(size) { - if (size != $rootScope.sizeRatio) { - $rootScope.sizeRatio = size; - document.getElementById('main').style.fontSize = size + 'em'; - Persist.setSizeRatio(size); - win.triggerHandler('resize'); - } - }; - - scope.resetTextSize = function() { - if ($rootScope.sizeRatio != 1) { - scope.textSizeChange(1); - scope.$broadcast('reset', 1); - } - }; - - scope.$watchCollection('allBuilds', function() { - scope.buildsList = Object.keys(scope.allBuilds).sort(); - }); - } - }; -}]); diff --git a/app/js/directives/directive-line-chart.js b/app/js/directives/directive-line-chart.js deleted file mode 100644 index 4ef45513..00000000 --- a/app/js/directives/directive-line-chart.js +++ /dev/null @@ -1,247 +0,0 @@ -angular.module('app').directive('lineChart', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) { - - var RENDER_POINTS = 20; // Only render 20 points on the graph - - return { - restrict: 'A', - scope: { - config: '=', - series: '=' - }, - link: function(scope, element) { - var seriesConfig = scope.series, - series = seriesConfig.series, - color = d3.scale.ordinal().range(scope.series.colors ? scope.series.colors : ['#ff8c0d']), - config = scope.config, - labels = config.labels, - margin = { top: 15, right: 15, bottom: 35, left: 60 }, - fmtLong = null, - func = seriesConfig.func, - drag = d3.behavior.drag(), - dragging = false, - // Define Scales - x = d3.scale.linear(), - y = d3.scale.linear(), - // Define Axes - xAxis = d3.svg.axis().scale(x).outerTickSize(0).orient('bottom'), - yAxis = d3.svg.axis().scale(y).ticks(6).outerTickSize(0).orient('left'), - data = []; - - // Create chart - var svg = d3.select(element[0]).append('svg'); - var vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); - var lines = vis.append('g'); - - // Define Area - var line = d3.svg.line().y(function(d) { return y(d[1]); }); - - // Create Y Axis SVG Elements - var yTxt = vis.append('g').attr('class', 'y axis') - .append('text') - .attr('class', 'cap') - .attr('transform', 'rotate(-90)') - .attr('y', -50) - .attr('dy', '.1em') - .style('text-anchor', 'middle'); - - // Create X Axis SVG Elements - var xLbl = vis.append('g').attr('class', 'x axis'); - var xTxt = xLbl.append('text') - .attr('class', 'cap') - .attr('y', 30) - .attr('dy', '.1em') - .style('text-anchor', 'middle'); - - // xTxt.append('tspan').attr('class', 'metric'); - // yTxt.append('tspan').attr('class', 'metric'); - - // Create and Add tooltip - var tipHeight = 2 + (1.25 * (series ? series.length : 0.75)); - var tips = vis.append('g').style('display', 'none').attr('class', 'tooltip'); - var markers = vis.append('g').style('display', 'none'); - - tips.append('rect') - .attr('height', tipHeight + 'em') - .attr('y', (-tipHeight / 2) + 'em') - .attr('class', 'tip'); - - tips.append('text') - .attr('class', 'label x') - .attr('dy', (-tipHeight / 2) + 'em') - .attr('y', '1.25em'); - - var background = vis.append('rect') // Background to capture hover/drag - .attr('fill-opacity', 0) - .on('mouseover', showTip) - .on('mouseout', hideTip) - .on('mousemove', moveTip) - .call(drag); - - drag - .on('dragstart', function() { - dragging = true; - moveTip.call(this); - showTip(); - }) - .on('dragend', function() { - dragging = false; - hideTip(); - }) - .on('drag', moveTip); - - updateFormats(); - - function render() { - var width = element[0].parentElement.offsetWidth, - height = width * 0.5 * $rootScope.sizeRatio, - xMax = seriesConfig.xMax, - xMin = seriesConfig.xMin, - yMax = seriesConfig.yMax, - yMin = seriesConfig.yMin, - w = width - margin.left - margin.right, - h = height - margin.top - margin.bottom, - c, s, val, yVal, delta; - - data.length = 0; // Reset Data array - - if (seriesConfig.xMax == seriesConfig.xMin) { - line.x(function(d, i) { return i * w; }); - } else { - line.x(function(d) { return x(d[0]); }); - } - - if (series) { - for (s = 0; s < series.length; s++) { - data.push([]); - } - - if (xMax == xMin) { - yVal = func(xMin); - for (s = 0; s < series.length; s++) { - data[s].push( [ xMin, yVal[ series[s] ] ], [ 1, yVal[ series[s] ] ]); - } - } else { - delta = (xMax - xMin) / RENDER_POINTS; - val = 0; - for (c = 0; c <= RENDER_POINTS; c++) { - yVal = func(val); - for (s = 0; s < series.length; s++) { - data[s].push([ val, yVal[ series[s] ] ]); - } - val += delta; - } - } - - } else { - var seriesData = []; - if (xMax == xMin) { - yVal = func(xMin); - seriesData.push([ xMin, yVal ], [ 1, yVal ]); - } else { - delta = (xMax - xMin) / RENDER_POINTS; - val = 0; - for (c = 0; c <= RENDER_POINTS; c++) { - seriesData.push([val, func(val) ]); - val += delta; - } - } - - data.push(seriesData); - } - - // Update Chart Size - svg.attr('width', width).attr('height', height); - background.attr('height', h).attr('width', w); - - // Update domain and scale for axes - x.range([0, w]).domain([xMin, xMax]).clamp(true); - xLbl.attr('transform', 'translate(0,' + h + ')'); - xTxt.attr('x', w / 2); - y.range([h, 0]).domain([yMin, yMax]); - yTxt.attr('x', -h / 2); - vis.selectAll('.y.axis').call(yAxis); - vis.selectAll('.x.axis').call(xAxis); - - lines.selectAll('path.line') - .data(data) - .attr('d', line) // Update existing series - .enter() // Add new series - .append('path') - .attr('class', 'line') - .attr('stroke', function(d, i) { return color(i); }) - .attr('stroke-width', 2) - .attr('d', line); - - tips.selectAll('text.label.y').data(data).enter() - .append('text') - .attr('class', 'label y') - .attr('dy', (-tipHeight / 2) + 'em') - .attr('y', function(d, i) { return 1.25 * (i + 2) + 'em'; }); - - markers.selectAll('circle.marker').data(data).enter().append('circle').attr('class', 'marker').attr('r', 4); - } - - function showTip() { - tips.style('display', null); - markers.style('display', null); - } - - function hideTip() { - if (!dragging) { - tips.style('display', 'none'); - markers.style('display', 'none'); - } - } - - function moveTip() { - var xPos = d3.mouse(this)[0], - x0 = x.invert(xPos), - y0 = func(x0), - yTotal = 0, - flip = (x0 / x.domain()[1] > 0.65), - tipWidth = 0, - minTransY = (tips.selectAll('rect').node().getBoundingClientRect().height / 2) - margin.top; - - tips.selectAll('text.label.y').text(function(d, i) { - var yVal = series ? y0[series[i]] : y0; - yTotal += yVal; - return (series ? $translate.instant(series[i]) : '') + ' ' + fmtLong(yVal); - }).append('tspan').attr('class', 'metric').text(' ' + $translate.instant(labels.yAxis.unit)); - - tips.selectAll('text').each(function() { - if (this.getBBox().width > tipWidth) { - tipWidth = Math.ceil(this.getBBox().width); - } - }); - - tipWidth += 8; - markers.selectAll('circle.marker').attr('cx', x(x0)).attr('cy', function(d, i) { return y(series ? y0[series[i]] : y0); }); - tips.selectAll('text.label').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); - tips.selectAll('text.label.x').text(fmtLong(x0)).append('tspan').attr('class', 'metric').text(' ' + $translate.instant(labels.xAxis.unit)); - tips.attr('transform', 'translate(' + x(x0) + ',' + Math.max(minTransY, y(yTotal / (series ? series.length : 1))) + ')'); - tips.selectAll('rect') - .attr('width', tipWidth + 4) - .attr('x', flip ? -tipWidth - 12 : 8) - .style('text-anchor', flip ? 'end' : 'start'); - - } - - function updateFormats() { - xTxt.text($translate.instant(labels.xAxis.title)).append('tspan').attr('class', 'metric').text(' (' + $translate.instant(labels.xAxis.unit) + ')'); - yTxt.text($translate.instant(labels.yAxis.title)).append('tspan').attr('class', 'metric').text(' (' + $translate.instant(labels.yAxis.unit) + ')'); - fmtLong = $rootScope.localeFormat.numberFormat('.2f'); - xAxis.tickFormat($rootScope.localeFormat.numberFormat('.2r')); - yAxis.tickFormat($rootScope.localeFormat.numberFormat('.3r')); - render(); - } - - angular.element($window).bind('orientationchange resize render', render); - scope.$watchCollection('series', render); // Watch for changes in the series data - scope.$on('languageChanged', updateFormats); - scope.$on('$destroy', function() { - angular.element($window).unbind('orientationchange resize render', render); - }); - - } - }; -}]); diff --git a/app/js/directives/directive-loader.js b/app/js/directives/directive-loader.js deleted file mode 100644 index b7743337..00000000 --- a/app/js/directives/directive-loader.js +++ /dev/null @@ -1,9 +0,0 @@ -angular.module('app').directive('loader', function() { - return { - restrict: 'A', - link: function(scope, element) { - element.addClass('loader'); - element.html(''); - } - }; -}); diff --git a/app/js/directives/directive-power-bands.js b/app/js/directives/directive-power-bands.js deleted file mode 100644 index 816c1ef2..00000000 --- a/app/js/directives/directive-power-bands.js +++ /dev/null @@ -1,209 +0,0 @@ -angular.module('app').directive('powerBands', ['$window', '$translate', '$rootScope', function($window, $translate, $rootScope) { - return { - restrict: 'A', - scope: { - bands: '=', - available: '=' - }, - link: function(scope, element) { - var bands = null, - available = 0, - maxBand, - maxPwr, - deployedSum = 0, - retractedSum = 0, - retBandsSelected = false, - depBandsSelected = false, - wattScale = d3.scale.linear(), - pctScale = d3.scale.linear().domain([0, 1]), - wattFmt, - pctFmt, - wattAxis = d3.svg.axis().scale(wattScale).outerTickSize(0).orient('top'), - pctAxis = d3.svg.axis().scale(pctScale).outerTickSize(0).orient('bottom'), - // Create chart - svg = d3.select(element[0]).append('svg'), - vis = svg.append('g'), - deployed = vis.append('g').attr('class', 'power-band'), - retracted = vis.append('g').attr('class', 'power-band'); - - svg.on('contextmenu', function() { - if (!d3.event.shiftKey) { - d3.event.preventDefault(); - for (var i = 0, l = bands.length; i < l; i++) { - bands[i].retSelected = false; - bands[i].depSelected = false; - } - dataChange(); - } - }); - - // Create Y Axis SVG Elements - var wattAxisGroup = vis.append('g').attr('class', 'watt axis'); - vis.append('g').attr('class', 'pct axis'); - var retText = vis.append('text').attr('x', -3).style('text-anchor', 'end').attr('dy', '0.5em').attr('class', 'primary upp'); - var depText = vis.append('text').attr('x', -3).style('text-anchor', 'end').attr('dy', '0.5em').attr('class', 'primary upp'); - var retLbl = vis.append('text').attr('dy', '0.5em'); - var depLbl = vis.append('text').attr('dy', '0.5em'); - - updateFormats(true); - - function dataChange() { - bands = scope.bands; - available = scope.available; - maxBand = bands[bands.length - 1]; - deployedSum = 0; - retractedSum = 0; - retBandsSelected = false; - depBandsSelected = false; - maxPwr = Math.max(available, maxBand.retractedSum, maxBand.deployedSum); - - for (var b = 0, l = bands.length; b < l; b++) { - if (bands[b].retSelected) { - retractedSum += bands[b].retracted + bands[b].retOnly; - retBandsSelected = true; - } - if (bands[b].depSelected) { - deployedSum += bands[b].deployed + bands[b].retracted; - depBandsSelected = true; - } - } - - render(); - } - - function render() { - var size = $rootScope.sizeRatio, - mTop = Math.round(25 * size), - mRight = Math.round(130 * size), - mBottom = Math.round(25 * size), - mLeft = Math.round(45 * size), - barHeight = Math.round(20 * size), - width = element[0].offsetWidth, - innerHeight = (barHeight * 2) + 2, - height = innerHeight + mTop + mBottom, - w = width - mLeft - mRight, - repY = (barHeight / 2), - depY = (barHeight * 1.5) - 1; - - // Update chart size - svg.attr('width', width).attr('height', height); - vis.attr('transform', 'translate(' + mLeft + ',' + mTop + ')'); - - // Remove existing elements - retracted.selectAll('rect').remove(); - retracted.selectAll('text').remove(); - deployed.selectAll('rect').remove(); - deployed.selectAll('text').remove(); - wattAxisGroup.selectAll('line.threshold').remove(); - - // Update X & Y Axis - wattScale.range([0, w]).domain([0, maxPwr]).clamp(true); - pctScale.range([0, w]).domain([0, maxPwr / available]).clamp(true); - wattAxisGroup.call(wattAxis); - vis.selectAll('.pct.axis').attr('transform', 'translate(0,' + innerHeight + ')').call(pctAxis); - - var pwrWarningClass = 'threshold' + (bands[0].retractedSum * 2 >= available ? ' exceeded' : ''); - vis.select('.pct.axis g:nth-child(6)').selectAll('line, text').attr('class', pwrWarningClass); - - wattAxisGroup.append('line') - .attr('x1', pctScale(0.5)) - .attr('x2', pctScale(0.5)) - .attr('y1', 0) - .attr('y2', innerHeight) - .attr('class', pwrWarningClass); - - retText.attr('y', repY); - depText.attr('y', depY); - updateLabel(retLbl, w, repY, retBandsSelected, retBandsSelected ? retractedSum : maxBand.retractedSum, available); - updateLabel(depLbl, w, depY, depBandsSelected, depBandsSelected ? deployedSum : maxBand.deployedSum, available); - - retracted.selectAll('rect').data(bands).enter().append('rect') - .attr('height', barHeight) - .attr('width', function(d) { return Math.ceil(Math.max(wattScale(d.retracted + d.retOnly), 0)); }) - .attr('x', function(d) { return Math.floor(Math.max(wattScale(d.retractedSum) - wattScale(d.retracted + d.retOnly), 0)); }) - .attr('y', 1) - .on('click', function(d) { - d.retSelected = !d.retSelected; - dataChange(); - }) - .attr('class', function(d) { return getClass(d.retSelected, d.retractedSum, available); }); - - retracted.selectAll('text').data(bands).enter().append('text') - .attr('x', function(d) { return wattScale(d.retractedSum) - (wattScale(d.retracted + d.retOnly) / 2); }) - .attr('y', repY) - .attr('dy', '0.5em') - .style('text-anchor', 'middle') - .attr('class', 'primary-bg') - .on('click', function(d) { - d.retSelected = !d.retSelected; - dataChange(); - }) - .text(function(d, i) { return bandText(d.retracted + d.retOnly, i); }); - - deployed.selectAll('rect').data(bands).enter().append('rect') - .attr('height', barHeight) - .attr('width', function(d) { return Math.ceil(Math.max(wattScale(d.deployed + d.retracted), 0)); }) - .attr('x', function(d) { return Math.floor(Math.max(wattScale(d.deployedSum) - wattScale(d.retracted) - wattScale(d.deployed), 0)); }) - .attr('y', barHeight + 1) - .on('click', function(d) { - d.depSelected = !d.depSelected; - dataChange(); - }) - .attr('class', function(d) { return getClass(d.depSelected, d.deployedSum, available); }); - - deployed.selectAll('text').data(bands).enter().append('text') - .attr('x', function(d) { return wattScale(d.deployedSum) - ((wattScale(d.retracted) + wattScale(d.deployed)) / 2); }) - .attr('y', depY) - .attr('dy', '0.5em') - .style('text-anchor', 'middle') - .attr('class', 'primary-bg') - .on('click', function(d) { - d.depSelected = !d.depSelected; - dataChange(); - }) - .text(function(d, i) { return bandText(d.deployed + d.retracted, i); }); - } - - function updateLabel(lbl, width, y, selected, sum, avail) { - lbl - .attr('x', width + 5 ) - .attr('y', y) - .attr('class', getClass(selected, sum, avail)) - .text(wattFmt(Math.max(0, sum)) + ' (' + pctFmt(Math.max(0, sum / avail)) + ')'); - } - - function getClass(selected, sum, avail) { - // Round to avoid floating point precision errors - return selected ? 'secondary' : ((Math.round(sum * 100) / 100) >= avail) ? 'warning' : 'primary'; - } - - function bandText(val, index) { - if (val > 0 && wattScale(val) > 13) { - return index + 1; - } - return ''; - } - - function updateFormats(preventRender) { - retText.text($translate.instant('ret')); - depText.text($translate.instant('dep')); - wattFmt = $rootScope.localeFormat.numberFormat('.2f'); - pctFmt = $rootScope.localeFormat.numberFormat('.1%'); - wattAxis.tickFormat($rootScope.localeFormat.numberFormat('.2r')); - pctAxis.tickFormat($rootScope.localeFormat.numberFormat('%')); - if (!preventRender) { - render(); - } - } - - // Watch for changes to data and events - angular.element($window).bind('pwrchange', dataChange); - angular.element($window).bind('orientationchange resize', render); - scope.$watchCollection('available', dataChange); - scope.$on('languageChanged', updateFormats); - scope.$on('$destroy', function() { - angular.element($window).unbind('orientationchange resize pwrchange', render); - }); - } - }; -}]); diff --git a/app/js/directives/directive-slider.js b/app/js/directives/directive-slider.js deleted file mode 100644 index 1931566e..00000000 --- a/app/js/directives/directive-slider.js +++ /dev/null @@ -1,90 +0,0 @@ -angular.module('app').directive('slider', ['$window', function($window) { - - return { - restrict: 'A', - scope: { - min: '=', - def: '=', - max: '=', - unit: '=', - change: '&onChange', - ignoreResize: '=' - }, - link: function(scope, element) { - var unit = scope.unit, - margin = unit ? { top: -10, right: 145, left: 50 } : { top: 0, right: 10, left: 10 }, - height = unit ? 40 : 20, // Height is fixed - h = height - margin.top, - fmt = d3.format('.2f'), - pct = d3.format('.1%'), - def = scope.def !== undefined ? scope.def : scope.max, - val = def, - svg = d3.select(element[0]).append('svg'), - vis = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'), - xAxisContainer = vis.append('g').attr('class', 'x slider-axis').attr('transform', 'translate(0,' + h / 2 + ')'), - x = d3.scale.linear(), - xAxis = d3.svg.axis().scale(x).orient('bottom').tickFormat(function(d) { return d + unit; }).tickSize(0).tickPadding(12), - slider = vis.append('g').attr('class', 'slider'), - filled = slider.append('path').attr('class', 'filled').attr('transform', 'translate(0,' + h / 2 + ')'), - brush = d3.svg.brush().x(x).extent([scope.max, scope.max]).on('brush', brushed), - handle = slider.append('circle').attr('class', 'handle').attr('r', '0.6em'), - lbl = unit ? slider.append('g').append('text').attr('y', h / 2) : null; - - slider.call(brush); - slider.select('.background').attr('height', h); - handle.attr('transform', 'translate(0,' + h / 2 + ')'); - - function render() { - var width = element[0].offsetWidth, w = width - margin.left - margin.right; - svg.attr('width', width).attr('height', height); - x.domain([scope.min || 0, scope.max]).range([0, w]).clamp(true); - handle.attr('cx', x(val)); - if (unit) { - xAxisContainer.call(xAxis.tickValues([0, scope.max / 4, scope.max / 2, (3 * scope.max) / 4, scope.max])); - lbl.attr('x', w + 20); - } - slider.call(brush.extent([val, val])); - drawBrush(); - slider.selectAll('.extent,.resize').remove(); - } - - function brushed() { - val = x.invert(d3.mouse(this)[0]); - brush.extent([val, val]); - scope.change({ val: val }); - drawBrush(); - } - - function drawBrush() { - if (unit) { - lbl.text(fmt(val) + ' ' + unit + ' ' + pct(val / scope.max)); - } - handle.attr('cx', x(val)); - filled.attr('d', 'M0,0V0H' + x(val) + 'V0'); - } - - /** - * Watch for changes in the max, window size - */ - scope.$watch('max', function(newMax, oldMax) { - val = newMax * (val / oldMax); // Retain percentage filled - scope.change({ val: val }); - render(); - }); - - if (!scope.ignoreResize) { - angular.element($window).bind('orientationchange resize', render); - } - - scope.$on('reset', function(e, resetVal) { - val = resetVal; - drawBrush(); - }); - - scope.$on('$destroy', function() { - angular.element($window).unbind('orientationchange resize render', render); - }); - - } - }; -}]); diff --git a/app/js/directives/directive-slot-hardpoint.js b/app/js/directives/directive-slot-hardpoint.js deleted file mode 100755 index dbfa50b8..00000000 --- a/app/js/directives/directive-slot-hardpoint.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('app').directive('slotHardpoint', ['$rootScope', function($r) { - return { - restrict: 'A', - scope: { - hp: '=', - size: '=' - }, - templateUrl: 'views/_slot-hardpoint.html', - link: function(scope) { - scope.$r = $r; - } - }; -}]); diff --git a/app/js/directives/directive-slot-internal.js b/app/js/directives/directive-slot-internal.js deleted file mode 100755 index afc148d6..00000000 --- a/app/js/directives/directive-slot-internal.js +++ /dev/null @@ -1,13 +0,0 @@ -angular.module('app').directive('slotInternal', ['$rootScope', function($r) { - return { - restrict: 'A', - scope: { - c: '=slot', - fuel: '=' - }, - templateUrl: 'views/_slot-internal.html', - link: function(scope) { - scope.$r = $r; - } - }; -}]); diff --git a/app/js/factory-utils.js b/app/js/factory-utils.js deleted file mode 100755 index 9eeba68d..00000000 --- a/app/js/factory-utils.js +++ /dev/null @@ -1,65 +0,0 @@ -/** - * BBCode Generator functions for embedding in the Elite Dangerous Forums - */ -angular.module('app').factory('Utils', ['$window', '$state', '$http', '$q', '$translate', '$rootScope', function($window, $state, $http, $q, $translate, $rootScope) { - - var shortenAPI = 'https://www.googleapis.com/urlshortener/v1/url?key='; - - function shortenUrl(url) { - if ($window.navigator.onLine) { - return $http.post(shortenAPI + GAPI_KEY, { longUrl: url }).then(function(response) { - return response.data.id; - }); - } else { - return $q.reject({ statusText: 'Not Online' }); - } - } - - function comparisonBBCode(facets, builds, link) { - var colCount = 2, b, i, j, k, f, fl, p, pl, l = []; - - for (i = 0; i < facets.length; i++) { - if (facets[i].active) { - f = facets[i]; - p = f.props; - - if (p.length == 1) { - l.push('[th][B][COLOR=#FF8C0D]', $translate.instant(f.title).toUpperCase(), '[/COLOR][/B][/th]'); - colCount++; - } else { - for (j = 0; j < p.length; j++) { - l.push('[th][B][COLOR=#FF8C0D]', $translate.instant(f.title).toUpperCase(), '\n', $translate.instant(f.lbls[j]).toUpperCase(), '[/COLOR][/B][/th]'); - colCount++; - } - } - } - } - l.push('[/tr]\n'); - - for (i = 0; i < builds.length; i++) { - b = builds[i]; - //var href = $state.href('outfit',{shipId: b.id, code: b.code, bn: b.buildName}, {absolute: true}); - l.push('[tr][td]', b.name, '[/td][td]', b.buildName, '[/td]'); - - for (j = 0, fl = facets.length; j < fl; j++) { - if (facets[j].active) { - f = facets[j]; - p = f.props; - for (k = 0, pl = p.length; k < pl; k++) { - l.push('[td="align: right"]', $rootScope[f.fmt](b[p[k]]), ' [size=-2]', $translate.instant(f.unit), '[/size][/td]'); - } - } - } - l.push('[/tr]\n'); - } - l.push('[tr][td="align: center, colspan:', colCount, '"][size=-3]\n[url=', link, ']Interactive Comparison at Coriolis.io[/url][/td][/tr]\n[/size][/table]'); - l.unshift('[table="width:', colCount * 90, ',align: center"]\n[tr][th][B][COLOR=#FF8C0D]Ship[/COLOR][/B][/th][th][B][COLOR="#FF8C0D"]Build[/COLOR][/B][/th]'); - return l.join(''); - } - - return { - comparisonBBCode: comparisonBBCode, - shortenUrl: shortenUrl - }; - -}]); diff --git a/app/js/i18n/de.js b/app/js/i18n/de.js deleted file mode 100644 index 4acc3b46..00000000 --- a/app/js/i18n/de.js +++ /dev/null @@ -1,220 +0,0 @@ -angular.module('app').config(['$translateProvider', 'localeFormatProvider', function($translateProvider, localeFormatProvider) { - // Declare number format settings - localeFormatProvider.addFormat('de', { - decimal: ',', - thousands: '.', - grouping: [3], - currency: ['', ' €'], - dateTime: '%A, der %e. %B %Y, %X', - date: '%d.%m.%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], // unused - days: ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'], - shortDays: ['So', 'Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa'], - months: ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'], - shortMonths: ['Jan', 'Feb', 'Mrz', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez'] - }); - $translateProvider.translations('de', { - PHRASE_EXPORT_DESC: 'Ein detaillierter JSON-Export Ihrer Konfiguration für die Verwendung in anderen Websites und Tools', - 'A-Rated': 'A-Klasse', - about: 'Über', - action: 'Aktion', - added: 'Hinzugefügt', - Advanced: 'Verbessert', - 'Advanced Discovery Scanner': 'Fortgeschrittener Aufklärungsscanner', - agility: 'Manövrierbarkeit', - ammo: 'Munition', - PHRASE_CONFIRMATION: 'Sind Sie sicher?', - armour: 'Panzerung', - am: 'Automatische Feldwartungs-Einheit', - available: 'Verfügbar', - backup: 'Sicherungsdatei', - 'Basic Discovery Scanner': 'Einfacher Aufklärungsscanner', - bl: 'Strahlenlaser', - beta: 'Beta', - bins: 'Behälter', - boost: 'Boost', - build: 'Ausstattung', - 'build name': 'Ausstattungsname', - builds: 'Ausstattungen', - bh: 'Rumpfhüllenverstärkung', - ul: 'Salvenlaser', - buy: 'Kaufen', - cancel: 'Abbrechen', - c: 'Kanone', - capital: 'Kapital', - cargo: 'Fracht', - 'Cargo Hatch': 'Frachtluke', - cr: 'Frachtgestell', - cs: 'Frachtscanner', - cells: 'Zellen', - 'Chaff Launcher': 'Düppel-Werfer', - close: 'Schließen', - cc: 'Krallensteuerung: Sammler', - compare: 'Vergleichen', - 'compare all': 'Alles Vergleichen', - comparison: 'Vergleich', - comparisons: 'Vergleiche', - component: 'Komponente', - cost: 'Preis', - costs: 'Kosten', - cm: 'Gegenmaßnahme', - create: 'Erstellen', - 'create new': 'Neu Erstellen', - Cytoscrambler: 'Zytostreuer', - damage: 'Schaden', - delete: 'Löschen', - 'delete all': 'Alles Löschen', - dep: 'Ausg', - deployed: 'Ausgefahren', - 'detailed export': 'Detailierter Export', - 'Detailed Surface Scanner': 'Detailoberflächenscanner', - disabled: 'Deaktiviert', - discount: 'Rabatt', - Distruptor: 'Disruptor', - dc: 'Standard-Landecomputer', - done: 'Fertig', - 'edit data': 'Bearbeiten', - efficiency: 'Effizienz', - 'Electronic Countermeasure': 'Elektronische Gegenmaßnahme', - empty: 'leer', - Enforcer: 'Vollstrecker', - ENG: 'ANT', - 'enter name': 'Namen eingeben', - export: 'Export', - fixed: 'Fixiert', - forum: 'Forum', - fc: 'Splitterkanone', - fd: 'Frameshiftantrieb', - ws: 'Frameshift-Sogwolkenscanner', - FSD: 'FSA', - fi: 'FSA-Unterbrecher', - fuel: 'Treibstoff', - fs: 'Treibstoffsammler', - ft: 'Treibstofftank', - fx: 'Krallensteuerung Treibstoffstransfer', - 'full tank': 'Tank voll', - Gimballed: 'Kardanisch', - H: 'R', - hardpoints: 'Waffenaufhängungen', - hb: 'Krallen-Steuereinheit (Ladelukenöffner)', - 'Heat Sink Launcher': 'Kühlkörperwerfer', - huge: 'Riesig', - hull: 'Hülle', - hr: 'Rumpfhüllenverstärkung (Paket)', - 'Imperial Hammer': 'Imperialer Hammer', - import: 'Importieren', - 'import all': 'Alles Importieren', - insurance: 'Versicherung', - 'Intermediate Discovery Scanner': 'Mittlerer Aufklärungsscanner', - 'internal compartments': 'Innenbereichskabine', - 'jump range': 'Sprungreichweite', - jumps: 'Sprünge', - kw: 'Tötungsbefehl-Scanner', - L: 'G', - laden: 'Beladen', - language: 'Sprache', - large: 'Groß', - limpets: 'Krallen', - ls: 'Lebenserhaltung', - 'Lightweight Alloy': 'Leichte Legierung', - 'lock factor': 'Massensperrefaktor', - LS: 'Ls', - LY: 'Lj', - mass: 'Masse', - 'max mass': 'maximale Masse', - medium: 'Mittel', - 'Military Grade Composite': 'Militär-Komposit', - nl: 'Minenwerfer', - 'Mining Lance': 'Lanzenabbaulaser', - ml: 'Abbaulaser', - 'Mirrored Surface Composite': 'Gespiegelte-Oberfläche-Komposit', - mr: 'Raketenbatterie', - mc: 'Mehrfachgeschütz', - 'net cost': 'Nettokosten', - no: 'Nein', - PHRASE_NO_BUILDS: 'Keine Konfigurationen zum Vergleich ausgewählt!', - PHRASE_NO_RETROCH: 'Keine Umrüständerungen', - none: 'Nichts', - 'none created': 'Leer', - off: 'Aus', - on: 'An', - 'optimal mass': 'optimale Masse', - 'optimize mass': 'Masse optimieren', - overwrite: 'Überschreiben', - Pacifier: 'Friedensstifter', - 'Pack-Hound': 'Schwarmwerfer', - PHRASE_IMPORT: 'JSON hier einfügen oder importieren', - pen: 'Durchdr.', - penetration: 'Durchdringung', - permalink: 'Permalink', - pa: 'Plasmabeschleuniger', - 'Point Defence': 'Punktverteidigung', - power: 'Energie', - pd: 'Energieverteiler', - pp: 'Kraftwerk', - pri: 'Prio', - priority: 'Priorität', - psg: 'Prismaschildgenerator', - proceed: 'Fortfahren', - pc: 'Krallensteuerung: Erzsucher', - pl: 'Impulslaser', - PWR: 'En', - rg: 'Schienenkanone', - range: 'Reichweite', - rate: 'Rate', - 'Reactive Surface Composite': 'Reaktive-Oberfläche-Komposit', - recharge: 'Aufladen', - rf: 'Raffinerie', - 'refuel time': 'Auftankzeit', - 'Reinforced Alloy': 'Verstärkte Legierung', - reload: 'Aktualisieren', - rename: 'Umbenennen', - repair: 'Reparieren', - reset: 'Zurücksetzen', - ret: 'Eing', - retracted: 'Eingefahren', - 'retrofit costs': 'Änderungskosten', - 'retrofit from': 'Nachrüsten von', - ROF: 'Kad', - S: 'K', - save: 'Speichern', - sc: 'Scanner', - PHRASE_SELECT_BUILDS: 'Ausstattung zum Vergleich auswählen', - sell: 'Verkaufen', - s: 'Sensoren', - settings: 'Einstellungen', - sb: 'Schild-Booster', - scb: 'Schildzellenbank', - sg: 'Schildgenerator', - shields: 'Schilde', - ship: 'Schiff', - ships: 'Schiffe', - shortened: 'Gekürzt', - size: 'Größe', - skip: 'Überspringen', - small: 'Klein', - speed: 'Geschwindigkeit', - standard: 'Standard', - 'Standard Docking Computer': 'Standard-Landecomputer', - Stock: 'Standard', - T: 't', - T_LOAD: 'T-Lad', - 'The Retributor': 'Retributor', - t: 'Schubdüsen', - time: 'Dauer', - tp: 'Torpedoaufhängung', - total: 'Gesamt', - 'total range': 'Maximale Reichweite', - turret: 'Geschützturm', - type: 'Typ', - U: 'W', - unladen: 'Unbeladen', - PHRASE_UPDATE_RDY: 'Update verfügbar! Klicken zum Aktualisieren', - utility: 'Werkzeug', - 'utility mounts': 'Werkzeug-Steckplätze', - WEP: 'WAF', - yes: 'Ja', - PHRASE_BACKUP_DESC: 'Export aller Coriolis-Daten, um sie zu sichern oder oder um sie zu einem anderen Browser/Gerät zu übertragen.' - }); -}]); diff --git a/app/js/i18n/en.js b/app/js/i18n/en.js deleted file mode 100644 index 3686433f..00000000 --- a/app/js/i18n/en.js +++ /dev/null @@ -1,219 +0,0 @@ -angular.module('app').config(['$translateProvider', function($translateProvider) { - $translateProvider.translations('en', { - PHRASE_EXPORT_DESC: 'A detailed JSON export of your build for use in other sites and tools', - 'A-Rated': 'A-Rated', - about: 'about', - action: 'action', - added: 'added', - Advanced: 'Advanced', - 'Advanced Discovery Scanner': 'Advanced Discovery Scanner', - agility: 'agility', - alpha: 'alpha', - ammo: 'ammo', - PHRASE_CONFIRMATION: 'Are You Sure?', - armour: 'armour', - am: 'Auto Field-Maintenance Unit', - available: 'available', - backup: 'backup', - 'Basic Discovery Scanner': 'Basic Discovery Scanner', - bl: 'Beam Laser', - beta: 'beta', - bins: 'bins', - boost: 'boost', - build: 'build', - 'build name': 'Build Name', - builds: 'builds', - bh: 'bulkheads', - 'bsg': 'Bi-Weave Shield Generator', - ul: 'Burst Laser', - buy: 'buy', - cancel: 'cancel', - c: 'Cannon', - capital: 'capital', - cargo: 'cargo', - 'Cargo Hatch': 'Cargo Hatch', - cr: 'Cargo Rack', - cs: 'Cargo Scanner', - cells: 'cells', - 'Chaff Launcher': 'Chaff Launcher', - close: 'close', - cc: 'Collector Limpet Controller', - compare: 'compare', - 'compare all': 'compare all', - comparison: 'comparison', - comparisons: 'comparisons', - component: 'component', - cost: 'cost', - costs: 'costs', - cm: 'Countermeasure', - CR: 'CR', - create: 'create', - 'create new': 'create new', - credits: 'credits', - Cytoscrambler: 'Cytoscrambler', - damage: 'damage', - delete: 'delete', - 'delete all': 'delete all', - dep: 'dep', - deployed: 'deployed', - 'detailed export': 'detailed export', - 'Detailed Surface Scanner': 'Detailed Surface Scanner', - disabled: 'disabled', - discount: 'discount', - Distruptor: 'Distruptor', - dc: 'Docking Computer', - done: 'done', - DPS: 'DPS', - 'edit data': 'edit data', - efficiency: 'efficiency', - 'Electronic Countermeasure': 'Electronic Countermeasure', - empty: 'empty', - Enforcer: 'Enforcer', - ENG: 'ENG', - 'enter name': 'Enter Name', - EPS: 'EPS', - export: 'export', - fixed: 'fixed', - forum: 'forum', - fc: 'Fragment Cannon', - fd: 'Frame Shift Drive', - ws: 'Frame Shift Wake Scanner', - FSD: 'FSD', - fi: 'FSD Interdictor', - fuel: 'fuel', - fs: 'Fuel Scoop', - ft: 'Fuel Tank', - fx: 'Fuel Transfer Limpet Controller', - 'full tank': 'full tank', - Gimballed: 'Gimballed', - H: 'H', - hardpoints: 'hardpoints', - hb: 'Hatch Breaker Limpet Controller', - 'Heat Sink Launcher': 'Heat Sink Launcher', - huge: 'huge', - hull: 'hull', - hr: 'Hull Reinforcement Package', - 'Imperial Hammer': 'Imperial Hammer', - import: 'import', - 'import all': 'import all', - insurance: 'insurance', - 'Intermediate Discovery Scanner': 'Intermediate Discovery Scanner', - 'internal compartments': 'internal compartments', - 'jump range': 'jump range', - jumps: 'jumps', - kw: 'Kill Warrant Scanner', - L: 'L', - laden: 'laden', - language: 'language', - large: 'large', - 'limpets': 'Limpets', - ls: 'life support', - 'Lightweight Alloy': 'Lightweight Alloy', - 'lock factor': 'lock factor', - LS: 'Ls', - LY: 'LY', - M: 'M', - 'm/s': 'm/s', - mass: 'mass', - max: 'max', - 'max mass': 'max mass', - medium: 'medium', - 'Military Grade Composite': 'Military Grade Composite', - nl: 'Mine Launcher', - 'Mining Lance': 'Mining Lance', - ml: 'Mining Laser', - 'Mirrored Surface Composite': 'Mirrored Surface Composite', - mr: 'Missile Rack', - mc: 'Multi-cannon', - 'net cost': 'net cost', - no: 'no', - PHRASE_NO_BUILDS: 'No builds added to comparison!', - PHRASE_NO_RETROCH: 'No Retrofitting changes', - none: 'none', - 'none created': 'none created', - off: 'off', - on: 'on', - optimal: 'optimal', - 'optimal mass': 'optimal mass', - 'optimize mass': 'optimize mass', - overwrite: 'overwrite', - Pacifier: 'Pacifier', - 'Pack-Hound': 'Pack-Hound', - PHRASE_IMPORT: 'Paste JSON or import here', - pen: 'pen', - penetration: 'penetration', - permalink: 'permalink', - pa: 'Plasma Accelerator', - 'Point Defence': 'Point Defence', - power: 'power', - pd: 'power distributor', - pp: 'power plant', - pri: 'pri', - priority: 'priority', - psg: 'Prismatic Shield Generator', - proceed: 'proceed', - pc: 'Prospector Limpet Controller', - pl: 'Pulse Laser', - pv: 'Planetary Vehicle Hangar', - PWR: 'PWR', - rg: 'Rail Gun', - range: 'range', - rate: 'rate', - 'Reactive Surface Composite': 'Reactive Surface Composite', - recharge: 'recharge', - rf: 'Refinery', - 'refuel time': 'refuel time', - 'Reinforced Alloy': 'Reinforced Alloy', - reload: 'reload', - rename: 'rename', - repair: 'repair', - reset: 'reset', - ret: 'ret', - retracted: 'retracted', - 'retrofit costs': 'retrofit costs', - 'retrofit from': 'retrofit from', - ROF: 'ROF', - S: 'S', - save: 'save', - sc: 'scanner', - PHRASE_SELECT_BUILDS: 'Select Builds to Compare', - sell: 'sell', - s: 'sensors', - settings: 'settings', - sb: 'Shield Booster', - scb: 'Shield Cell Bank', - sg: 'Shield Generator', - shields: 'shields', - ship: 'ship', - ships: 'ships', - shortened: 'shortened', - size: 'size', - skip: 'skip', - small: 'small', - speed: 'speed', - standard: 'standard', - 'Standard Docking Computer': 'Standard Docking Computer', - Stock: 'Stock', - SYS: 'SYS', - T: 'T', - T_LOAD: 't-load', - 'The Retributor': 'The Retributor', - t: 'thrusters', - time: 'time', - tp: 'Torpedo Pylon', - total: 'total', - 'total range': 'total range', - turret: 'turret', - type: 'type', - U: 'U', - unladen: 'unladen', - PHRASE_UPDATE_RDY: 'Update Available! Click to Refresh', - URL: 'URL', - utility: 'utility', - 'utility mounts': 'utility mounts', - version: 'version', - WEP: 'WEP', - yes: 'yes', - PHRASE_BACKUP_DESC: 'Backup of all Coriolis data to save or transfer to another browser/device' - }); -}]); diff --git a/app/js/i18n/es.js b/app/js/i18n/es.js deleted file mode 100644 index ccc5bbac..00000000 --- a/app/js/i18n/es.js +++ /dev/null @@ -1,212 +0,0 @@ -angular.module('app').config(['$translateProvider', 'localeFormatProvider', function($translateProvider, localeFormatProvider) { - - // Declare number format settings - localeFormatProvider.addFormat('es', { - decimal: ',', - thousands: '.', - grouping: [3], - currency: ['', ' €'], - dateTime: '%A, %e de %B de %Y, %X', - date: '%d/%m/%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], - days: ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'], - shortDays: ['dom', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb'], - months: ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'], - shortMonths: ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic'] - }); - - $translateProvider.translations('es', { - 'PHRASE_EXPORT_DESC': 'Una detallada exportaci\u00f3n JSON de tu construcci\u00f3n para usarlo en otros sitios web y herramientas', - 'A-Rated': 'Calidad-A', - 'about': 'Acerca', - 'action': 'Acci\u00f3n', - 'added': 'A\u00f1adido', - 'Advanced Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n avanzado', - 'agility': 'Maniobrabilidad', - 'alpha': 'Alfa', - 'ammo': 'Munici\u00f3n', - 'PHRASE_CONFIRMATION': '\u00bfEst\u00e1s seguro?', - 'armour': 'Blindaje', - 'am': 'Unidad de auto-reparaciones', - 'available': 'Disponible', - 'backup': 'Reserva', - 'Basic Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n b\u00e1sico', - 'bl': 'L\u00e1ser de haz', - 'bins': 'contenedores', - 'boost': 'incrementar', - 'build': 'Construcci\u00f3n', - 'build name': 'Nombre de la construcci\u00f3n', - 'builds': 'Construcciones', - 'bh': 'mamparos', - 'ul': 'Laser de r\u00e1fagas', - 'buy': 'Comprar', - 'cancel': 'Cancelar', - 'c': 'Ca\u00f1\u00f3n', - 'capital': 'capital', - 'cargo': 'Carga', - 'Cargo Hatch': 'Compuerta de carga', - 'cr': 'Compartimento de carga', - 'cs': 'Esc\u00e1ner de carga', - 'cells': 'celdas', - 'Chaff Launcher': 'Lanzador de birutas', - 'close': 'Cerrar', - 'cc': 'Controlador de Drones de Recogida', - 'compare': 'Comparar', - 'compare all': 'comparar todas', - 'comparison': 'Comparativa', - 'comparisons': 'Comparativas', - 'component': 'Componente', - 'cost': 'Coste', - 'costs': 'Costes', - 'cm': 'Contramedidas', - 'create': 'Crear', - 'create new': 'Crear nuevo', - 'credits': 'Cr\u00e9ditos', - 'damage': 'Da\u00f1o', - 'delete': 'Borrar', - 'delete all': 'Borrar todo', - 'dep': 'desp', - 'deployed': 'Desplegado', - 'detailed export': 'Exportacion detallada', - 'Detailed Surface Scanner': 'Escaner de exploraci\u00f3n detallada', - 'disabled': 'Desactivado', - 'discount': 'Descuento', - 'dc': 'Ordenador de aterrizaje', - 'done': 'Hecho', - 'DPS': 'DPS (Da\u00f1o Por Segundo)', - 'edit data': 'Editar datos', - 'efficiency': 'Eficiencia', - 'Electronic Countermeasure': 'Contramedidas electr\u00f3nicas', - 'empty': 'Vac\u00edo', - 'ENG': 'MOT', - 'enter name': 'Introduce el nombre', - 'export': 'exportar', - 'fixed': 'fijo', - 'forum': 'Foro', - 'fc': 'Ca\u00f1\u00f3n de fragmentaci\u00f3n', - 'fd': 'Motor de salto', - 'ws': 'Esc\u00e1ner de Salto', - 'fi': 'Interdictor FSD', - 'fuel': 'Combustible', - 'fs': 'Recolector de Combustible', - 'ft': 'Tanque de combustible', - 'fx': 'Sistema de Transferencia de Combustilble', - 'full tank': 'Tanque lleno', - 'Gimballed': 'Card\u00e1n', - 'H': 'E', - 'hardpoints': 'Montura de armas', - 'hb': 'Controlador de Apertura de Bah\u00eda de Carga', - 'Heat Sink Launcher': 'Eyector de Acumulador de Calor', - 'huge': 'enorme', - 'hull': 'Casco', - 'hr': 'Sistema de Casco Reforzado', - 'import': 'Importar', - 'import all': 'Importar todo', - 'insurance': 'Seguro', - 'Intermediate Discovery Scanner': 'Esc\u00e1ner de exploraci\u00f3n media', - 'internal compartments': 'Compartimentos internos', - 'jump range': 'Rango de salto', - 'jumps': 'Saltos', - 'kw': 'Esc\u00e1ner Detector de Recompensas', - 'L': 'G', - 'laden': 'Cargada', - 'language': 'Idioma', - 'large': 'Grande', - 'ls': 'Soporte vital', - 'Lightweight Alloy': 'Aleaci\u00f3n ligera', - 'limpets': 'Drones', - 'lock factor': 'factor de bloqueo', - 'mass': 'Masa', - 'max': 'm\u00e1x', - 'max mass': 'Masa m\u00e1xima', - 'medium': 'medio', - 'Military Grade Composite': 'Blindaje Militar', - 'nl': 'Lanzaminas', - 'Mining Lance': 'Lanza de miner\u00eda', - 'ml': 'L\u00e1ser de miner\u00eda', - 'Mirrored Surface Composite': 'Blindaje Reflectante', - 'mr': 'Bah\u00eda de Misiles', - 'mc': 'Ca\u00f1\u00f3n m\u00faltiple', - 'net cost': 'Coste neto', - 'PHRASE_NO_BUILDS': '\u00a1No se a\u00f1adieron plantillas para comparaci\u00f3n!', - 'PHRASE_NO_RETROCH': 'No hay cambios en los ajutes', - 'none': 'Nada', - 'none created': 'Nada creado', - 'off': 'apagado', - 'on': 'encendido', - 'optimal': '\u00f3ptimo', - 'optimal mass': 'masa \u00f3ptima', - 'optimize mass': 'optimizar masa', - 'overwrite': 'Sobreescribir', - 'PHRASE_IMPORT': 'Pega el JSON o imp\u00f3rtalo aqu\u00ed', - 'penetration': 'penetraci\u00f3n', - 'permalink': 'enlace permanente', - 'pa': 'Acelerador de Plasma', - 'Point Defence': 'Punto de Defensa', - 'power': 'energ\u00eda', - 'pd': 'distribuidor de energ\u00eda', - 'pp': 'Planta de Energ\u00eda', - 'priority': 'prioridad', - 'proceed': 'Proceder', - 'pc': 'Controlador de drones de prospecci\u00f3n', - 'pl': 'L\u00e1ser de Pulso', - 'PWR': 'POT', - 'rg': 'Ca\u00f1\u00f3n de Riel', - 'range': 'rango', - 'rate': 'ratio', - 'Reactive Surface Composite': 'Blindaje Reactivo', - 'recharge': 'recargar', - 'rf': 'Refineria', - 'refuel time': 'Tiempo para repostar', - 'Reinforced Alloy': 'Armadura reforzada', - 'reload': 'Recargar', - 'rename': 'Renombrar', - 'repair': 'Reparar', - 'reset': 'Reiniciar', - 'ret': 'PLE', - 'retracted': 'plegadas', - 'retrofit costs': 'costes de equipamiento', - 'retrofit from': 'equipamiento desde', - 'ROF': 'RDF', - 'S': 'P', - 'save': 'guardar', - 'sc': 'sc\u00e1ner', - 'PHRASE_SELECT_BUILDS': 'Selecciona equipamientos para comparar', - 'sell': 'Vender', - 's': 'Sensores', - 'settings': 'Configuraci\u00f3n', - 'sb': 'Potenciador de Escudos', - 'scb': 'C\u00e9lula de Energ\u00eda de Escudos', - 'sg': 'Generador de escudos', - 'shields': 'Escudos', - 'ship': 'Nave ', - 'ships': 'Naves', - 'shortened': 'Abreviado', - 'size': 'Tama\u00f1o', - 'skip': 'omitir', - 'small': 'Peque\u00f1o', - 'speed': 'velocidad', - 'standard': 'est\u00e1ndar', - 'Standard Docking Computer': 'Computador de Atraque Est\u00e1ndar', - 'Stock': 'De serie', - 'SYS': 'SIS', - 'T_LOAD': 'c-t\u00e9rmica', - 't': 'Propulsores', - 'time': 'Tiempo', - 'tp': 'Anclaje de torpedo', - 'total': 'Total', - 'total range': 'Rango total', - 'turret': 'torreta', - 'type': 'Tipo', - 'unladen': 'Sin carga', - 'PHRASE_UPDATE_RDY': 'Actualizacion disponible! Haz click para recargar', - 'URL': 'Enlace', - 'utility': 'utilidad', - 'utility mounts': 'monturas de utilidad', - 'version': 'Versi\u00f3n', - 'WEP': 'ARM', - 'yes': 'si', - 'PHRASE_BACKUP_DESC': 'Copia de seguridad de todos los datos de Coriolis para guardarlos o transferirlos a otro navegador\/dispositivo' - }); -}]); diff --git a/app/js/i18n/fr.js b/app/js/i18n/fr.js deleted file mode 100644 index aa82e502..00000000 --- a/app/js/i18n/fr.js +++ /dev/null @@ -1,200 +0,0 @@ -angular.module('app').config(['$translateProvider', 'localeFormatProvider', function($translateProvider, localeFormatProvider) { - // Declare number format settings - localeFormatProvider.addFormat('fr', { - decimal: ',', - thousands: '.', - grouping: [3], - currency: ['', ' €'], - dateTime: '%A, le %e %B %Y, %X', - date: '%d/%m/%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], // unused - days: ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'], - shortDays: ['dim.', 'lun.', 'mar.', 'mer.', 'jeu.', 'ven.', 'sam.'], - months: ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'], - shortMonths: ['janv.', 'févr.', 'mars', 'avr.', 'mai', 'juin', 'juil.', 'août', 'sept.', 'oct.', 'nov.', 'déc.'] - }); - $translateProvider.translations('fr', { - PHRASE_EXPORT_DESC: 'Export détaillé en JSON de votre configuration pour utilisation sur d\'autres sites et outils', - 'A-Rated': 'Classe-A ', - about: 'à propos', - added: 'ajouté', - Advanced: 'Avancé', - 'Advanced Discovery Scanner': 'Détecteur découverte avancé', - agility: 'manœuvrabilité', - ammo: 'munitions', - PHRASE_CONFIRMATION: 'Êtes-vous sûr ?', - armour: 'Armure', - am: 'Unité de maintenance de terrain auto', - available: 'Disponibilité', - backup: 'sauvegarde', - 'Basic Discovery Scanner': 'Détecteur découverte simple', - bl: 'Rayon Laser', - bins: 'bennes', - build: 'Configuration', - 'build name': 'Nom de la configuration', - builds: 'Configurations', - bh: 'Coque', - ul: 'Laser à rafale', - buy: 'Acheter', - cancel: 'Annuler', - c: 'Canon', - cargo: 'Soute', - 'Cargo Hatch': 'Écoutille de soute', - cr: 'Compartiment de soute', - cs: 'Détecteur de cargaison', - cells: 'Cellules', - 'Chaff Launcher': 'Lanceur de paillettes', - close: 'fermer', - cc: 'Contrôleur de Collecteur', - compare: 'comparer', - 'compare all': 'tout comparer', - comparison: 'comparaison', - comparisons: 'comparaisons', - component: 'composant', - cost: 'coût', - costs: 'coûts', - cm: 'Contre-mesure', - create: 'Créer', - 'create new': 'Créer nouveau', - credits: 'crédits', - damage: 'Dégâts', - delete: 'supprimer', - 'delete all': 'tout supprimer', - dep: 'depl', - deployed: 'déployé', - 'detailed export': 'export détaillé', - 'Detailed Surface Scanner': 'Détecteur surface détaillé', - disabled: 'désactivé', - discount: 'réduction', - Distruptor: 'Disrupteur', - dc: 'Ordinateur d\'appontage', - done: 'Valider', - 'edit data': 'Editer donnée', - efficiency: 'efficacité', - 'Electronic Countermeasure': 'Contre-mesures électroniques', - empty: 'Vide', - 'enter name': 'Entrer un nom', - fixed: 'fixé', - fc: 'Canon à fragmentation', - fd: 'Réacteur FSD', - ws: 'Détecteur de sillage FSD', - fi: 'Intercepteur de réacteur FSD', - fuel: 'carburant', - fs: 'Récupérateur de carburant', - ft: 'Réservoir de carburant', - fx: 'Contrôleur de ravitailleur', - 'full tank': 'Réservoir plein', - Gimballed: 'Point', - hardpoints: 'Points d\'emport', - hb: 'Contrôle de patelle perce-soute', - 'Heat Sink Launcher': 'Éjecteur de dissipateur thermique', - huge: 'Très grand', - hull: 'Coque', - hr: 'Ensemble de mesures permettant de renforcer la coque', - 'Imperial Hammer': 'Marteau impérial', - import: 'Importer', - 'import all': 'Importer tout', - insurance: 'Assurance', - 'Intermediate Discovery Scanner': 'Détecteur découverte intermédiaire', - 'internal compartments': 'compartiments internes', - 'jump range': 'Distance de saut', - jumps: 'Sauts', - kw: 'Détecteur d\'avis de recherche', - laden: 'chargé', - language: 'Langue', - large: 'large', - ls: 'Systèmes de survie', - 'Lightweight Alloy': 'alliage léger', - 'limpets': 'Patelles', - 'lock factor': 'facteur inhibition de masse', - LS: 'SL', - LY: 'AL', - mass: 'Masse', - 'max mass': 'masse max', - 'Military Grade Composite': 'Composite militaire', - nl: 'Lance-mines', - 'Mining Lance': 'Lance de minage', - ml: 'Laser minier', - 'Mirrored Surface Composite': 'Composite à surface miroir', - mr: 'Batterie de missiles', - mc: 'Canon multiple', - 'net cost': 'coûts nets', - no: 'non', - PHRASE_NO_BUILDS: 'Aucune configuration ajoutée pour comparaison', - PHRASE_NO_RETROCH: 'Configuration non modifiée', - none: 'aucun', - 'none created': 'Rien de créé', - off: 'éteint', - on: 'allumé', - 'optimal mass': 'masse optimale', - 'optimize mass': 'optimiser masse', - overwrite: 'remplacer', - Pacifier: 'Pacificateur', - PHRASE_IMPORT: 'Coller ici votre JSON à importer', - pen: 'pén.', - penetration: 'pénétration', - permalink: 'lien durable', - pa: 'accélérateur plasma', - 'Point Defence': 'Défense ponctuelle', - power: 'énergie', - pd: 'Répartiteur de puissance', - pp: 'Générateur', - priority: 'priorité', - psg: 'générateur de bouclier prisme', - proceed: 'continuer', - pc: 'Contrôleur de prospecteur', - pl: 'Laser à impulsion', - PWR: 'P', - rg: 'Canon électrique', - range: 'portée', - rate: 'cadence', - 'Reactive Surface Composite': 'Composite à surface réactive', - recharge: 'recharger', - rf: 'Raffinerie', - 'refuel time': 'Temps de remplissage', - 'Reinforced Alloy': 'alliage renforcé', - reload: 'recharger', - rename: 'renommer', - repair: 'réparer', - reset: 'Réinitialisation', - ret: 'esc', - retracted: 'escamoté', - 'retrofit costs': 'Valeur de rachat', - 'retrofit from': 'Racheter de', - ROF: 'cadence', - save: 'sauvegarder', - sc: 'scanner', - PHRASE_SELECT_BUILDS: 'Sélectionner les configurations à comparer', - sell: 'vendre', - s: 'Capteurs', - settings: 'paramètres', - sb: 'Survolteur de bouclier', - scb: 'Réserve de cellules d\'énergie', - sg: 'Générateur de bouclier', - shields: 'boucliers', - ship: 'vaisseau', - ships: 'vaisseaux', - shortened: 'raccourci', - size: 'taille', - skip: 'Suivant', - small: 'petit', - speed: 'vitesse', - 'Standard Docking Computer': 'Ordinateur d\'appontage standard', - Stock: 'de base', - T_LOAD: 'Charge thermique', - 'The Retributor': 'Le Rétributeur', - t: 'propulseurs', - time: 'temps', - tp: 'Tube lance-torpille', - 'total range': 'Distance maximale', - turret: 'tourelle', - unladen: 'Non chargé', - PHRASE_UPDATE_RDY: 'Mise à jour disponible ! Cliquez ici pour mettre à jour', - utility: 'utilitaire', - 'utility mounts': 'Support utilitaire', - WEP: 'ARM', - yes: 'oui', - PHRASE_BACKUP_DESC: 'Exportation détaillée des données de Coriolis pour l\'utilisation dans d\'autres sites et outils' - }); -}]); diff --git a/app/js/i18n/it.js b/app/js/i18n/it.js deleted file mode 100644 index 8c5248f7..00000000 --- a/app/js/i18n/it.js +++ /dev/null @@ -1,133 +0,0 @@ -angular.module('app').config(['$translateProvider', 'localeFormatProvider', function($translateProvider, localeFormatProvider) { - - // Declare number format settings - localeFormatProvider.addFormat('es', { - decimal: ',', - thousands: '.', - grouping: [3], - currency: ['€', ''], - dateTime: '%A %e %B %Y, %X', - date: '%d/%m/%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], // unused - days: ['Domenica', 'Lunedì', 'Martedì', 'Mercoledì', 'Giovedì', 'Venerdì', 'Sabato'], - shortDays: ['Dom', 'Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab'], - months: ['Gennaio', 'Febbraio', 'Marzo', 'Aprile', 'Maggio', 'Giugno', 'Luglio', 'Agosto', 'Settembre', 'Ottobre', 'Novembre', 'Dicembre'], - shortMonths: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic'] - }); - - $translateProvider.translations('it', { - PHRASE_EXPORT_DESC: 'Un export dettagliato in formato JSON della tua configurazione per essere usato in altri siti o tools', - 'A-Rated': 'Classe A', - about: 'Info su Coriolis', - action: 'azione', - added: 'aggiunto', - Advanced: 'Avanzato', - agility: 'agilità', - ammo: 'munizioni', - PHRASE_CONFIRMATION: 'Sei sicuro ?', - armour: 'armatura', - available: 'disponibile', - bins: 'contenitore', - build: 'configurazione', - 'build name': 'Nome Configurazione', - builds: 'configurazioni', - buy: 'compra', - cancel: 'cancella', - cells: 'celle', - close: 'chiudi', - compare: 'confronta', - 'compare all': 'confronta tutti', - comparison: 'comparazione', - comparisons: 'comparazioni', - component: 'componente', - cost: 'costo', - costs: 'costi', - cm: 'Contromisure', - create: 'crea', - 'create new': 'crea nuovo', - credits: 'crediti', - damage: 'danno', - delete: 'elimina', - 'delete all': 'elimina tutto', - dep: 'dep', - deployed: 'deployed', - 'detailed export': 'esportazione dettagliata', - disabled: 'disabilita', - discount: 'sconto', - done: 'fatto', - 'edit data': 'modifica i dati', - efficiency: 'efficenza', - empty: 'vuoto', - Enforcer: 'Rinforzatore', - 'enter name': 'Inserisci un nome', - export: 'esporta', - fixed: 'fissi', - fuel: 'carburante', - 'full tank': 'Serbatoio Pieno', - huge: 'enorme', - hull: 'corazza', - import: 'importa', - 'import all': 'importa tutto', - insurance: 'assicurazione', - 'internal compartments': 'compartimenti interni', - 'jump range': 'distanza di salto', - jumps: 'salti', - laden: 'carico', - language: 'lingua', - large: 'largo', - mass: 'massa', - max: 'massimo', - 'max mass': 'massa massimale', - medium: 'medio', - 'net cost': 'costo netto', - PHRASE_NO_BUILDS: 'nessuna configurazione è stata aggiunta per la comparazione!', - PHRASE_NO_RETROCH: 'Nessun cambiamento di Retrofitting', - none: 'nessuno', - 'none created': 'nessuno creato', - optimal: 'ottimale', - 'optimal mass': 'massa ottimale', - 'optimize mass': 'ottimizza la massa', - overwrite: 'sovrasscrivi', - PHRASE_IMPORT: 'Incolla un JSON o importalo qua', - penetration: 'penetrazione', - power: 'potenza', - priority: 'priorità', - proceed: 'procedi', - range: 'distanza', - rate: 'rateo', - recharge: 'ricarica', - reload: 'ricarica', - rename: 'rinomina', - repair: 'ripara', - reset: 'resetta', - retracted: 'retratti', - 'retrofit costs': 'costi di retrofit', - 'retrofit from': 'retrofit da', - save: 'salva', - sell: 'vendi', - settings: 'impostazioni', - shields: 'scudi', - ship: 'nave', - ships: 'navi', - shortened: 'accorciato', - size: 'grandezza', - skip: 'salta', - small: 'piccolo', - speed: 'velocità', - Stock: 'appena comprata', - t: 'thrusters', - time: 'tempo', - total: 'totale', - 'total range': 'distanza totale', - turret: 'torrette', - type: 'tipo', - unladen: 'scarico', - PHRASE_UPDATE_RDY: 'Aggiornamenti disponibili ! Clicca per Aggiornare', - utility: 'supporti', - 'utility mounts': 'supporti di utilità', - version: 'versione', - yes: 'sì', - PHRASE_BACKUP_DESC: 'Esportazione di tutti i dati su Coriolis per salvarli o trasferirli in un altro Browser/dispositivo' - }); -}]); diff --git a/app/js/i18n/languages.js b/app/js/i18n/languages.js deleted file mode 100644 index 9b9ca5f4..00000000 --- a/app/js/i18n/languages.js +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('app').config(['$translateProvider', function($translateProvider) { - $translateProvider - .useSanitizeValueStrategy('escapeParameters') - .useStorage('Persist') - .fallbackLanguage('en') // Use English as default/fallback language - .registerAvailableLanguageKeys(['en', 'de', 'es', 'fr', 'it', 'ru'], { - 'en*': 'en', - 'de*': 'de', - 'es*': 'es', - 'fr*': 'fr', - 'it*': 'it', - 'ru*': 'ru' - }) - .determinePreferredLanguage(); -}]) -.value('Languages', { - en: 'English', - de: 'Deutsch', - it: 'Italiano', - es: 'Español', - fr: 'Français', - ru: 'ру́сский' -}); diff --git a/app/js/i18n/ru.js b/app/js/i18n/ru.js deleted file mode 100644 index e6b61363..00000000 --- a/app/js/i18n/ru.js +++ /dev/null @@ -1,234 +0,0 @@ -angular.module('app').config(['$translateProvider', 'localeFormatProvider', function($translateProvider, localeFormatProvider) { - - // Declare number format settings - localeFormatProvider.addFormat('ru', { - decimal: ',', - thousands: '\xa0', - grouping: [3], - currency: ['', ' руб.'], - dateTime: '%A, %e %B %Y г. %X', - date: '%d.%m.%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], - days: ['воскресенье', 'понедельник', 'вторник', 'среда', 'четверг', 'пятница', 'суббота'], - shortDays: ['вс', 'пн', 'вт', 'ср', 'чт', 'пт', 'сб'], - months: ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня', 'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'], - shortMonths: ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'] - }); - - $translateProvider.translations('ru', { - PHRASE_EXPORT_DESC: 'Подробный экспорта JSON вашего телосложения для использования в других местах и инструментов', - 'A-Rated': 'А-Класса', - about: 'О сайте', - action: 'Действие', - added: 'Добавлено', - Advanced: 'Продвинутый', - 'Advanced Discovery Scanner': 'Продвинутый астросканер', - agility: 'Маневренность', - alpha: 'Альфа', - ammo: 'Боекомплект', - PHRASE_CONFIRMATION: 'Вы уверены?', - armour: 'Броня', - am: 'Ремонтный модуль', - available: 'доступно', - backup: 'Резервная копия', - 'Basic Discovery Scanner': 'Стандартный исследовательский сканер', - bl: 'Лучевой лазер', - beta: 'Бета', - bins: 'контейнеры', - boost: 'форсаж', - build: 'cборка', - 'build name': 'название сборки', - builds: 'cборки', - bh: 'Корпус', - ul: 'Мультиимпульсный лазер', - buy: 'купить', - cancel: 'отменить', - c: 'Пушка', - capital: 'Крупный', - cargo: 'Груз', - 'Cargo Hatch': 'Грузовой люк', - cr: 'Грузовой отсек', - cs: 'Сканер груза', - cells: 'Ячейки', - 'Chaff Launcher': 'Постановщик помех', - close: 'закрыть', - cc: 'Контроллер "дрон-сборщик"', - compare: 'сравнить ', - 'compare all': 'сравнить все', - comparison: 'сравнение', - comparisons: 'сравнения', - component: 'Компонент', - cost: 'Стоимость', - costs: 'Расходы', - cm: 'Контрмеры', - CR: 'кр.', - create: 'создать', - 'create new': 'Создать новый', - credits: 'Кредиты', - Cytoscrambler: 'сайтоскрамблер', - damage: 'Урон', - delete: 'Удалить', - 'delete all': 'Удалить все', - dep: 'Вып', - deployed: 'Открыты', - 'detailed export': 'Подробный экспорт', - 'Detailed Surface Scanner': 'Подробный сканер поверхности', - disabled: 'Отключено', - discount: 'Скидка', - Distruptor: 'Дисраптор', - dc: 'Стыковочный компьютер', - done: 'готово', - DPS: 'УВС', - 'edit data': 'Редактирование', - efficiency: 'Эффективность', - 'Electronic Countermeasure': 'Электронная противомера', - empty: 'пусто', - Enforcer: 'Энфорсер', - ENG: 'ДВГ', - 'enter name': 'Введите имя', - EPS: 'ЭВС', - export: 'Экспорт', - fixed: 'Фиксированое', - forum: 'Форум', - fc: 'Осколочное Орудие', - fd: 'Двигатель FSD', - ws: 'FSD Сканнер', - - fi: 'Перехватчик FSD', - fuel: 'Топливо', - fs: 'Топливосборщик', - ft: 'Топливный бак', - fx: 'Контроллер Дрона-заправщика', - 'full tank': 'Полный бак', - Gimballed: 'Шарнирное', - H: 'O', - hardpoints: 'Орудийные порты', - hb: 'Контроллер "дрон-взломщик"', - 'Heat Sink Launcher': 'Теплоотводная ПУ', - huge: 'огромный', - hull: 'Корпус', - hr: 'Набор усиления корпуса', - 'Imperial Hammer': 'Имперский Молот', - import: 'импортировать ', - 'import all': 'импортировать все', - insurance: 'Страховка', - 'Intermediate Discovery Scanner': 'Средний исследовательский сканер', - 'internal compartments': 'внутренние отсеки', - 'jump range': 'Дальность прыжка', - jumps: 'Прыжков', - kw: 'Полицейский сканер', - L: 'б', - laden: 'Груженый', - language: 'Язык', - large: 'большой', - ls: 'Система жизнеобеспечения', - 'Lightweight Alloy': 'Легкий сплав', - 'limpets': 'Дроны', - 'lock factor': 'Масс. блок', - LS: 'Св.сек', - LY: 'Св.лет', - M: 'С', - 'm/s': 'м/с', - mass: 'Масса', - max: 'Макс', - 'max mass': 'Максимальная масса', - medium: 'Средний', - 'Military Grade Composite': 'Военный композит', - nl: 'Минноукладчик', - 'Mining Lance': 'Бурильная сулица', - ml: 'Бурильный лазер', - 'Mirrored Surface Composite': 'Зеркальный композит', - mr: 'Ракетная установка', - mc: 'Многоствольное орудие', - 'net cost': 'разница в цене', - no: 'Нет', - PHRASE_NO_BUILDS: 'Нечего сравнивать', - PHRASE_NO_RETROCH: 'нет ранних версий сборки\конфигурации', - none: 'ни один', - 'none created': 'не создано', - off: 'выкл', - on: 'вкл', - optimal: 'Оптимальный', - 'optimal mass': 'Оптимальная масса', - 'optimize mass': 'Оптимизировать массу', - overwrite: 'перезаписать', - Pacifier: 'Миротворец', - 'Pack-Hound': 'Ракета "Гончая"', - PHRASE_IMPORT: 'Для импорта вставьте код в эту форму', - pen: 'ПБ', - penetration: 'Пробитие', - permalink: 'Постоянная ссылка', - pa: 'Ускоритель плазмы', - 'Point Defence': 'Противоракетная защита', - power: 'Мощность', - pd: 'Распределитель энергии', - pp: 'Реактор', - pri: 'Осн', - priority: 'Приоритет', - psg: 'Генератор призматического щита', - proceed: 'продолжить', - pc: 'Контроллер "Дрон-исследователь"', - pl: 'Импульсный лазер', - PWR: 'Эн', - rg: 'Рельсотрон', - range: 'Дальность', - rate: 'скорость', - 'Reactive Surface Composite': 'Динамическая защита', - recharge: 'Перезарядка', - rf: 'Переработка', - 'refuel time': 'Время дозаправки', - 'Reinforced Alloy': 'Усиленный сплав', - reload: 'Перезарядить', - rename: 'Переименовать', - repair: 'Починка', - reset: 'Сброс', - ret: 'Убр.', - retracted: 'Убрано', - 'retrofit costs': 'цена модификации', - 'retrofit from': 'модификация от', - ROF: 'В/сек', - S: 'М', - save: 'Сохранить', - sc: 'Сканер', - PHRASE_SELECT_BUILDS: 'Выберите конфигурацию для сравнения', - sell: 'Продать', - s: 'Сенсоры', - settings: 'Настройки', - sb: 'Усилитель щита', - scb: 'Батареи перезарядки щита', - sg: 'Генератор щита', - shields: 'Щиты', - ship: 'Корабль', - ships: 'Корабли', - shortened: 'Укороченный', - size: 'размер', - skip: 'пропустить', - small: 'Малый', - speed: 'скорость', - standard: 'Стандартный', - 'Standard Docking Computer': 'Стандартный стыковочный компьютер', - Stock: 'Стандартная комплектация', - SYS: 'СИС', - T: 'Т', - T_LOAD: 'Тепл.', - 'The Retributor': '"Возмездие"', - t: 'Двигатели', - time: 'Время', - tp: 'Торпедный аппарат', - total: 'Всего', - 'total range': 'Общий радиус', - turret: 'Туррель', - type: 'Тип', - U: 'В', - unladen: 'Пустой', - PHRASE_UPDATE_RDY: 'Доступно обновление. Нажмите для обновления.', - URL: 'Ссылка', - utility: 'Вспомогательное', - 'utility mounts': 'Вспомогательное оборудование', - version: 'Версия', - WEP: 'ОРУ', - yes: 'Да', - PHRASE_BACKUP_DESC: 'Сохраните все данные перед переносом в другой браузер или устройство' - }); -}]); diff --git a/app/js/provider-locale-format.js b/app/js/provider-locale-format.js deleted file mode 100644 index 598cd813..00000000 --- a/app/js/provider-locale-format.js +++ /dev/null @@ -1,36 +0,0 @@ - -angular.module('app').provider('localeFormat', function localeFormatProvider() { - var formats = { - en: { - decimal: '.', - thousands: ',', - grouping: [3], - currency: ['$', ''], - dateTime: '%a %b %e %X %Y', - date: '%m/%d/%Y', - time: '%H:%M:%S', - periods: ['AM', 'PM'], - days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], - shortDays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], - months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], - shortMonths: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - } - }; - - function LocaleFormat(formatMap) { - this.formatMap = formatMap; - - this.get = function(lang) { - return this.formatMap[lang] ? this.formatMap[lang] : this.formatMap.en; - }; - } - - this.addFormat = function(langCode, formatDetails) { - formats[langCode] = formatDetails; - }; - - this.$get = [function() { - return new LocaleFormat(formats); - }]; - -}); diff --git a/app/js/service-persist.js b/app/js/service-persist.js deleted file mode 100755 index bcca430c..00000000 --- a/app/js/service-persist.js +++ /dev/null @@ -1,314 +0,0 @@ -/** - * [description] - */ -angular.module('app').service('Persist', ['$window', 'lodash', function($window, _) { - var LS_KEY_BUILDS = 'builds'; - var LS_KEY_COMPARISONS = 'comparisons'; - var LS_KEY_LANG = 'NG_TRANSLATE_LANG_KEY'; - var LS_KEY_COST_TAB = 'costTab'; - var LS_KEY_INSURANCE = 'insurance'; - var LS_KEY_DISCOUNTS = 'discounts'; - var localStorage = $window.localStorage; - var buildJson = null; - var comparisonJson = null; - - // Safe check to determine if localStorage is enabled - try { - localStorage.setItem('s', 1); - localStorage.removeItem('s'); - buildJson = localStorage.getItem(LS_KEY_BUILDS); - comparisonJson = localStorage.getItem(LS_KEY_COMPARISONS); - this.lsEnabled = true; - } catch(e) { - this.lsEnabled = false; - } - - this.builds = buildJson ? angular.fromJson(buildJson) : {}; - this.comparisons = comparisonJson ? angular.fromJson(comparisonJson) : {}; - var buildCount = Object.keys(this.builds).length; - - this.state = { - buildCount: buildCount, - hasBuilds: buildCount > 0, - hasComparisons: Object.keys(this.comparisons).length > 0 - }; - - this.put = function(name, value) { - if (!this.lsEnabled) { - return; - } - localStorage.setItem(name, value); - }; - - this.get = function(name) { - return this.lsEnabled ? localStorage.getItem(name) : null; - }; - - this.getLangCode = function() { - return this.lsEnabled ? localStorage.getItem(LS_KEY_LANG) : null; - }; - - /** - * Persist a ship build in local storage. - * - * @param {string} shipId The unique id for a model of ship - * @param {string} name The name of the build - * @param {string} code The serialized code - */ - this.saveBuild = function(shipId, name, code) { - if (!this.lsEnabled) { - return; - } - - if (!this.builds[shipId]) { - this.builds[shipId] = {}; - } - - if (!this.builds[shipId][name]) { - this.state.buildCount++; - this.state.hasBuilds = true; - } - - this.builds[shipId][name] = code; - // Persist updated build collection to localStorage - localStorage.setItem(LS_KEY_BUILDS, angular.toJson(this.builds)); - }; - - /** - * Get the serialized code/string for a build. Returns null if a - * build is not found. - * - * @param {string} shipId The unique id for a model of ship - * @param {string} name The name of the build - * @return {string} The serialized build string. - */ - this.getBuild = function(shipId, name) { - if (this.builds[shipId] && this.builds[shipId][name]) { - return this.builds[shipId][name]; - } - return null; - }; - - /** - * Delete a build from local storage. It will also delete the ship build collection if - * it becomes empty - * - * @param {string} shipId The unique id for a model of ship - * @param {string} name The name of the build - */ - this.deleteBuild = function(shipId, name) { - if (this.lsEnabled && this.builds[shipId][name]) { - delete this.builds[shipId][name]; - if (Object.keys(this.builds[shipId]).length === 0) { - delete this.builds[shipId]; - this.state.buildCount--; - this.state.hasBuilds = this.state.buildCount > 0; - } - // Persist updated build collection to localStorage - localStorage.setItem(LS_KEY_BUILDS, angular.toJson(this.builds)); - // Check if the build was used in existing comparisons - var comps = this.comparisons; - for (var c in comps) { - for (var i = 0; i < comps[c].builds.length; i++) { // For all builds in the current comparison - if (comps[c].builds[i].shipId == shipId && comps[c].builds[i].buildName == name) { - comps[c].builds.splice(i, 1); - break; // A build is unique ber comparison - } - } - } - localStorage.setItem(LS_KEY_COMPARISONS, angular.toJson(this.comparisons)); - } - }; - - /** - * Persist a comparison in localstorage. - * - * @param {string} name The name of the comparison - * @param {array} builds Array of builds - * @param {array} facets Array of facet indices - */ - this.saveComparison = function(name, builds, facets) { - if (!this.lsEnabled) { - return; - } - - if (!this.comparisons[name]) { - this.comparisons[name] = {}; - } - this.comparisons[name] = { - facets: facets, - builds: _.map(builds, function(b) { return { shipId: b.id || b.shipId, buildName: b.buildName }; }) - }; - localStorage.setItem(LS_KEY_COMPARISONS, angular.toJson(this.comparisons)); - this.state.hasComparisons = true; - }; - - /** - * [getComparison description] - * @param {string} name [description] - * @return {object} Object containing array of facets and ship id + build names - */ - this.getComparison = function(name) { - if (this.comparisons[name]) { - return this.comparisons[name]; - } - return null; - }; - - /** - * Removes the comparison from localstorage. - * @param {string} name Comparison name - */ - this.deleteComparison = function(name) { - if (this.lsEnabled && this.comparisons[name]) { - delete this.comparisons[name]; - localStorage.setItem(LS_KEY_COMPARISONS, angular.toJson(this.comparisons)); - this.state.hasComparisons = Object.keys(this.comparisons).length > 0; - } - }; - - /** - * Delete all builds and comparisons from localStorage - */ - this.deleteAll = function() { - angular.copy({}, this.builds); // Empty object but keep original instance - angular.copy({}, this.comparisons); - this.state.hasBuilds = false; - this.state.buildCount = 0; - - if (this.lsEnabled) { - localStorage.removeItem(LS_KEY_BUILDS); - localStorage.removeItem(LS_KEY_COMPARISONS); - } - }; - - this.getAll = function() { - var data = {}; - data[LS_KEY_BUILDS] = this.builds; - data[LS_KEY_COMPARISONS] = this.comparisons; - data[LS_KEY_INSURANCE] = this.getInsurance(); - data[LS_KEY_DISCOUNTS] = this.getDiscount(); - - return data; - }; - - /** - * Get the saved insurance type - * @return {string} The name of the saved insurance type of null - */ - this.getInsurance = function() { - if (this.lsEnabled) { - return localStorage.getItem(LS_KEY_INSURANCE); - } - return null; - }; - - /** - * Persist selected insurance type - * @param {string} name Insurance type name - */ - this.setInsurance = function(name) { - if (this.lsEnabled) { - return localStorage.setItem(LS_KEY_INSURANCE, name); - } - }; - - /** - * Persist selected discount - * @param {number} val Discount value/amount - */ - this.setDiscount = function(val) { - if (this.lsEnabled) { - return localStorage.setItem(LS_KEY_DISCOUNTS, angular.toJson(val)); - } - }; - - /** - * Get the saved discount - * @return {number} val Discount value/amount - */ - this.getDiscount = function() { - if (this.lsEnabled) { - return angular.fromJson(localStorage.getItem(LS_KEY_DISCOUNTS)); - } - return null; - }; - - /** - * Persist selected cost tab - * @param {number} val Discount value/amount - */ - this.setCostTab = function(tabName) { - if (this.lsEnabled) { - return localStorage.setItem(LS_KEY_COST_TAB, tabName); - } - }; - - /** - * Get the saved discount - * @return {number} val Discount value/amount - */ - this.getCostTab = function() { - if (this.lsEnabled) { - return localStorage.getItem(LS_KEY_COST_TAB); - } - return null; - }; - - /** - * Retrieve the last router state from local storage - * @return {object} state State object containing state name and params - */ - this.getState = function() { - if (this.lsEnabled) { - var state = localStorage.getItem('state'); - if (state) { - return angular.fromJson(state); - } - } - return null; - }; - - /** - * Save the current router state to localstorage - * @param {object} state State object containing state name and params - */ - this.setState = function(state) { - if (this.lsEnabled) { - localStorage.setItem('state', angular.toJson(state)); - } - }; - - /** - * Retrieve the last router state from local storage - * @return {number} size Ratio - */ - this.getSizeRatio = function() { - if (this.lsEnabled) { - var ratio = localStorage.getItem('sizeRatio'); - if (!isNaN(ratio) && ratio > 0.6) { - return ratio; - } - } - return 1; - }; - - /** - * Save the current size ratio to localstorage - * @param {number} sizeRatio - */ - this.setSizeRatio = function(sizeRatio) { - if (this.lsEnabled) { - localStorage.setItem('sizeRatio', sizeRatio); - } - }; - - /** - * Check if localStorage is enabled/active - * @return {Boolean} True if localStorage is enabled - */ - this.isEnabled = function() { - return this.lsEnabled; - }; - -}]); diff --git a/app/js/service-serializer.js b/app/js/service-serializer.js deleted file mode 100755 index 753d616d..00000000 --- a/app/js/service-serializer.js +++ /dev/null @@ -1,245 +0,0 @@ -/** - * Service managing seralization and deserialization of models for use in URLs and persistene. - */ -angular.module('app').service('Serializer', ['lodash', 'GroupMap', 'MountMap', 'ShipsDB', 'Ship', 'Components', '$state', function(_, GroupMap, MountMap, ShipsDB, Ship, Components, $state) { - - /** - * Serializes the ships selected components for all slots to a URL friendly string. - * @param {Ship} ship The ship to be serialized. - * @return {string} Encoded string of components - */ - this.fromShip = function(ship) { - var power = { - enabled: [ship.cargoHatch.enabled ? 1 : 0], - priorities: [ship.cargoHatch.priority] - }; - - var data = [ - ship.bulkheads.id, - _.map(ship.standard, mapGroup, power), - _.map(ship.hardpoints, mapGroup, power), - _.map(ship.internal, mapGroup, power), - '.', - LZString.compressToBase64(power.enabled.join('')).replace(/\//g, '-'), - '.', - LZString.compressToBase64(power.priorities.join('')).replace(/\//g, '-') - ]; - - return _.flatten(data).join(''); - }; - - /** - * Updates an existing ship instance's slots with components determined by the - * code. - * - * @param {Ship} ship The ship instance to be updated - * @param {string} dataString The string to deserialize - */ - this.toShip = function(ship, dataString) { - var standard = new Array(ship.standard.length), - hardpoints = new Array(ship.hardpoints.length), - internal = new Array(ship.internal.length), - parts = dataString.split('.'), - priorities = null, - enabled = null, - code = parts[0]; - - if (parts[1]) { - enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split(''); - } - - if (parts[2]) { - priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split(''); - } - - decodeToArray(code, internal, decodeToArray(code, hardpoints, decodeToArray(code, standard, 1))); - - ship.buildWith( - { - bulkheads: code.charAt(0) * 1, - standard: standard, - hardpoints: hardpoints, - internal: internal - }, - priorities, - enabled - ); - }; - - this.toDetailedBuild = function(buildName, ship, code) { - var standard = ship.standard, - hardpoints = ship.hardpoints, - internal = ship.internal; - - var data = { - $schema: 'http://cdn.coriolis.io/schemas/ship-loadout/3.json#', - name: buildName, - ship: ship.name, - references: [{ - name: 'Coriolis.io', - url: $state.href('outfit', { shipId: ship.id, code: code, bn: buildName }, { absolute: true }), - code: code, - shipId: ship.id - }], - components: { - standard: { - bulkheads: ship.bulkheads.c.name, - cargoHatch: { enabled: Boolean(ship.cargoHatch.enabled), priority: ship.cargoHatch.priority + 1 }, - powerPlant: { class: standard[0].c.class, rating: standard[0].c.rating, enabled: Boolean(standard[0].enabled), priority: standard[0].priority + 1 }, - thrusters: { class: standard[1].c.class, rating: standard[1].c.rating, enabled: Boolean(standard[1].enabled), priority: standard[1].priority + 1 }, - frameShiftDrive: { class: standard[2].c.class, rating: standard[2].c.rating, enabled: Boolean(standard[2].enabled), priority: standard[2].priority + 1 }, - lifeSupport: { class: standard[3].c.class, rating: standard[3].c.rating, enabled: Boolean(standard[3].enabled), priority: standard[3].priority + 1 }, - powerDistributor: { class: standard[4].c.class, rating: standard[4].c.rating, enabled: Boolean(standard[4].enabled), priority: standard[4].priority + 1 }, - sensors: { class: standard[5].c.class, rating: standard[5].c.rating, enabled: Boolean(standard[5].enabled), priority: standard[5].priority + 1 }, - fuelTank: { class: standard[6].c.class, rating: standard[6].c.rating, enabled: Boolean(standard[6].enabled), priority: standard[6].priority + 1 } - }, - hardpoints: _.map(_.filter(hardpoints, function(slot) { return slot.maxClass > 0; }), slotToSchema), - utility: _.map(_.filter(hardpoints, function(slot) { return slot.maxClass === 0; }), slotToSchema), - internal: _.map(internal, slotToSchema) - }, - stats: {} - }; - - for (var stat in ship) { - if (!isNaN(ship[stat])) { - data.stats[stat] = Math.round(ship[stat] * 100) / 100; - } - } - - return data; - }; - - this.fromDetailedBuild = function(detailedBuild) { - var shipId = _.findKey(ShipsDB, { properties: { name: detailedBuild.ship } }); - - if (!shipId) { - throw 'No such ship: ' + detailedBuild.ship; - } - - var comps = detailedBuild.components; - var standard = comps.standard; - var priorities = [ standard.cargoHatch && standard.cargoHatch.priority !== undefined ? standard.cargoHatch.priority - 1 : 0 ]; - var enabled = [ standard.cargoHatch && standard.cargoHatch.enabled !== undefined ? standard.cargoHatch.enabled : true ]; - var shipData = ShipsDB[shipId]; - var ship = new Ship(shipId, shipData.properties, shipData.slots); - var bulkheads = Components.bulkheadIndex(standard.bulkheads); - - if (bulkheads < 0) { - throw 'Invalid bulkheads: ' + standard.bulkheads; - } - - var standardIds = _.map( - ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank'], - function(c) { - if (!standard[c].class || !standard[c].rating) { - throw 'Invalid value for ' + c; - } - priorities.push(standard[c].priority === undefined ? 0 : standard[c].priority - 1); - enabled.push(standard[c].enabled === undefined ? true : standard[c].enabled); - return standard[c].class + standard[c].rating; - } - ); - - var internal = _.map(comps.internal, function(c) { return c ? Components.findInternalId(c.group, c.class, c.rating, c.name) : 0; }); - - var hardpoints = _.map(comps.hardpoints, function(c) { - return c ? Components.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount], c.missile) : 0; - }).concat(_.map(comps.utility, function(c) { - return c ? Components.findHardpointId(c.group, c.class, c.rating, c.name, MountMap[c.mount]) : 0; - })); - - // The ordering of these arrays must match the order in which they are read in Ship.buildWith - priorities = priorities.concat(_.map(comps.hardpoints, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }), - _.map(comps.utility, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; }), - _.map(comps.internal, function(c) { return (!c || c.priority === undefined) ? 0 : c.priority - 1; })); - enabled = enabled.concat(_.map(comps.hardpoints, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; }), - _.map(comps.utility, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; }), - _.map(comps.internal, function(c) { return (!c || c.enabled === undefined) ? true : c.enabled * 1; })); - - ship.buildWith({ bulkheads: bulkheads, standard: standardIds, hardpoints: hardpoints, internal: internal }, priorities, enabled); - - return ship; - }; - - this.toDetailedExport = function(builds) { - var data = []; - - for (var shipId in builds) { - for (var buildName in builds[shipId]) { - var code = builds[shipId][buildName]; - var shipData = ShipsDB[shipId]; - var ship = new Ship(shipId, shipData.properties, shipData.slots); - this.toShip(ship, code); - data.push(this.toDetailedBuild(buildName, ship, code)); - } - } - return data; - }; - - this.fromComparison = function(name, builds, facets, predicate, desc) { - var shipBuilds = []; - - builds.forEach(function(b) { - shipBuilds.push({ s: b.id, n: b.buildName, c: this.fromShip(b) }); - }.bind(this)); - - return LZString.compressToBase64(angular.toJson({ - n: name, - b: shipBuilds, - f: facets, - p: predicate, - d: desc ? 1 : 0 - })).replace(/\//g, '-'); - }; - - this.toComparison = function(code) { - return angular.fromJson(LZString.decompressFromBase64(code.replace(/-/g, '/'))); - }; - - /** - * Utility function to retrieve a safe string for selected component for a slot. - * Used for serialization to code only. - * - * @private - * @param {object} slot The slot object. - * @return {string} The id of the selected component or '-' if none selected - */ - function mapGroup(slot) { - this.enabled.push(slot.enabled ? 1 : 0); - this.priorities.push(slot.priority); - - return slot.id === null ? '-' : slot.id; - } - - function decodeToArray(code, arr, codePos) { - for (var i = 0; i < arr.length; i++) { - if (code.charAt(codePos) == '-') { - arr[i] = 0; - codePos++; - } else { - arr[i] = code.substring(codePos, codePos + 2); - codePos += 2; - } - } - return codePos; - } - - function slotToSchema(slot) { - if (slot.c) { - var o = { class: slot.c.class, rating: slot.c.rating, enabled: Boolean(slot.enabled), priority: slot.priority + 1, group: GroupMap[slot.c.grp] }; - if (slot.c.name) { - o.name = slot.c.name; - } - if (slot.c.mode) { - o.mount = MountMap[slot.c.mode]; - } - if (slot.c.missile) { - o.missile = slot.c.missile; - } - return o; - } - return null; - } - - -}]); diff --git a/app/js/shipyard/factory-component-set.js b/app/js/shipyard/factory-component-set.js deleted file mode 100755 index d58ff7a1..00000000 --- a/app/js/shipyard/factory-component-set.js +++ /dev/null @@ -1,145 +0,0 @@ -angular.module('shipyard').factory('ComponentSet', ['lodash', function(_) { - - function filter(data, maxClass, minClass, mass) { - return _.filter(data, function(c) { - return c.class <= maxClass && c.class >= minClass && (c.maxmass === undefined || mass <= c.maxmass); - }); - } - - function getKey(maxClass, eligible) { - if (eligible) { - return maxClass + Object.keys(eligible).join('-'); - } - return maxClass; - } - - function ComponentSet(components, mass, maxStandardArr, maxInternal, maxHardPoint) { - this.mass = mass; - this.standard = {}; - this.internal = {}; - this.hardpoints = {}; - this.hpClass = {}; - this.intClass = {}; - - this.standard[0] = filter(components.standard[0], maxStandardArr[0], 0, mass); // Power Plant - this.standard[2] = filter(components.standard[2], maxStandardArr[2], 0, mass); // FSD - this.standard[4] = filter(components.standard[4], maxStandardArr[4], 0, mass); // Power Distributor - this.standard[6] = filter(components.standard[6], maxStandardArr[6], 0, mass); // Fuel Tank - - // Thrusters, filter components by class only (to show full list of ratings for that class) - var minThrusterClass = _.reduce(components.standard[1], function(minClass, thruster) { - return (thruster.maxmass >= mass && thruster.class < minClass) ? thruster.class : minClass; - }, maxStandardArr[1]); - this.standard[1] = filter(components.standard[1], maxStandardArr[1], minThrusterClass, 0); // Thrusters - - // Slots where component class must be equal to slot class - this.standard[3] = filter(components.standard[3], maxStandardArr[3], maxStandardArr[3], 0); // Life Supprt - this.standard[5] = filter(components.standard[5], maxStandardArr[5], maxStandardArr[5], mass); // Sensors - - for (var h in components.hardpoints) { - this.hardpoints[h] = filter(components.hardpoints[h], maxHardPoint, 0, mass); - } - - for (var g in components.internal) { - this.internal[g] = filter(components.internal[g], maxInternal, 0, mass); - } - - /** - * Create a memoized function for determining the components that are - * eligible for an internal slot - * @param {integer} c The max class component that can be mounted in the slot - * @param {Object} eligible) The map of eligible internal groups - * @return {object} A map of all eligible components by group - */ - this.getInts = _.memoize( - function(c, eligible) { - var o = {}; - for (var key in this.internal) { - if (eligible && !eligible[key]) { - continue; - } - var data = filter(this.internal[key], c, 0, this.mass); - if (data.length) { // If group is not empty - o[key] = data; - } - } - return o; - }, - getKey - ); - - /** - * Create a memoized function for determining the components that are - * eligible for an hardpoint slot - * @param {integer} c The max class component that can be mounted in the slot - * @param {Object} eligible) The map of eligible hardpoint groups - * @return {object} A map of all eligible components by group - */ - this.getHps = _.memoize( - function(c, eligible) { - var o = {}; - for (var key in this.hardpoints) { - if (eligible && !eligible[key]) { - continue; - } - var data = filter(this.hardpoints[key], c, c ? 1 : 0, this.mass); - if (data.length) { // If group is not empty - o[key] = data; - } - } - return o; - }, - getKey - ); - } - - ComponentSet.prototype.lightestPowerDist = function(boostEnergy) { - var pds = this.standard[4]; - var pd = pds[0]; - - for (var i = 1; i < pds.length; i++) { - if (pds[i].mass < pd.mass && pds[i].enginecapacity >= boostEnergy) { - pd = pds[i]; - } - } - return pd.class + pd.rating; - }; - - ComponentSet.prototype.lightestThruster = function(ladenMass) { - var ths = this.standard[1]; - var th = ths[0]; - - for (var i = 1; i < ths.length; i++) { - if (ths[i].mass < th.mass && ths[i].maxmass >= ladenMass) { - th = ths[i]; - } - } - return th.class + th.rating; - }; - - ComponentSet.prototype.lightestShieldGenerator = function(hullMass) { - var sg = null; - - _.forEach(this.internal.sg, function(s) { - if (sg == null || (s.mass < sg.mass && s.minmass <= hullMass && s.maxmass > hullMass)) { - sg = s; - } - }); - return sg.id; - }; - - ComponentSet.prototype.lightestPowerPlant = function(powerUsed, rating) { - var pps = this.standard[0]; - var pp = null; - - for (var i = 0; i < pps.length; i++) { - if (pp == null || (pps[i].mass < pp.mass && pps[i].pGen >= powerUsed)) { - pp = pps[i]; - } - } - return pp.class + (pp.rating != 'D' || rating == 'A' ? 'A' : 'D'); // Use A rated if C,E - }; - - return ComponentSet; - -}]); diff --git a/app/js/shipyard/factory-ship.js b/app/js/shipyard/factory-ship.js deleted file mode 100755 index d42bee02..00000000 --- a/app/js/shipyard/factory-ship.js +++ /dev/null @@ -1,600 +0,0 @@ -angular.module('shipyard').factory('Ship', ['Components', 'calcShieldStrength', 'calcJumpRange', 'calcTotalRange', 'calcSpeed', 'lodash', 'ArmourMultiplier', function(Components, calcShieldStrength, calcJumpRange, calcTotalRange, calcSpeed, _, ArmourMultiplier) { - - /** - * Returns the power usage type of a slot and it's particular component - * @param {object} slot The Slot - * @param {object} component The component in the slot - * @return {string} The key for the power usage type - */ - function powerUsageType(slot, component) { - if (component) { - if (component.passive) { - return 'retracted'; - } - } - return slot.cat != 1 ? 'retracted' : 'deployed'; - } - - /** - * Ship model used to track all ship components and properties. - * - * @param {string} id Unique ship Id / Key - * @param {object} properties Basic ship properties such as name, manufacturer, mass, etc - * @param {object} slots Collection of slot groups (standard/standard, internal, hardpoints) with their max class size. - */ - function Ship(id, properties, slots) { - this.id = id; - this.cargoHatch = { c: Components.cargoHatch(), type: 'SYS' }; - this.bulkheads = { incCost: true, maxClass: 8 }; - this.availCS = Components.forShip(id); - - for (var p in properties) { this[p] = properties[p]; } // Copy all base properties from shipData - - for (var slotType in slots) { // Initialize all slots - var slotGroup = slots[slotType]; - var group = this[slotType] = []; // Initialize Slot group (Standard, Hardpoints, Internal) - for (var i = 0; i < slotGroup.length; i++) { - if (typeof slotGroup[i] == 'object') { - group.push({ id: null, c: null, incCost: true, maxClass: slotGroup[i].class, eligible: slotGroup[i].eligible }); - } else { - group.push({ id: null, c: null, incCost: true, maxClass: slotGroup[i] }); - } - } - } - // Make a Ship 'slot'/item similar to other slots - this.c = { incCost: true, type: 'SHIP', discountedCost: this.hullCost, c: { name: this.name, cost: this.hullCost } }; - - this.costList = _.union(this.internal, this.standard, this.hardpoints); - this.costList.push(this.bulkheads); // Add The bulkheads - this.costList.unshift(this.c); // Add the ship itself to the list - - this.powerList = _.union(this.internal, this.hardpoints); - this.powerList.unshift(this.cargoHatch); - this.powerList.unshift(this.standard[1]); // Add Thrusters - this.powerList.unshift(this.standard[5]); // Add Sensors - this.powerList.unshift(this.standard[4]); // Add Power Distributor - this.powerList.unshift(this.standard[3]); // Add Life Support - this.powerList.unshift(this.standard[2]); // Add FSD - this.powerList.unshift(this.standard[0]); // Add Power Plant - - this.shipCostMultiplier = 1; - this.componentCostMultiplier = 1; - - this.priorityBands = [ - { deployed: 0, retracted: 0, retOnly: 0 }, - { deployed: 0, retracted: 0, retOnly: 0 }, - { deployed: 0, retracted: 0, retOnly: 0 }, - { deployed: 0, retracted: 0, retOnly: 0 }, - { deployed: 0, retracted: 0, retOnly: 0 } - ]; - } - - //*********// - // GETTERS // - //*********// - - Ship.prototype.getAvailableComponents = function() { - return this.availCS; - }; - - Ship.prototype.getSlotStatus = function(slot, deployed) { - if (!slot.c) { // Empty Slot - return 0; // No Status (Not possible to be active in this state) - } else if (!slot.enabled) { - return 1; // Disabled - } else if (deployed) { - return this.priorityBands[slot.priority].deployedSum >= this.powerAvailable ? 2 : 3; // Offline : Online - // Active hardpoints have no retracted status - } else if ((slot.cat === 1 && !slot.c.passive)) { - return 0; // No Status (Not possible to be active in this state) - } - return this.priorityBands[slot.priority].retractedSum >= this.powerAvailable ? 2 : 3; // Offline : Online - }; - -/** - * Calculate jump range using the installed FSD and the - * specified mass which can be more or less than ships actual mass - * @param {number} mass Mass in tons - * @param {number} fuel Fuel available in tons - * @return {number} Jump range in Light Years - */ - Ship.prototype.getJumpRangeForMass = function(mass, fuel) { - return calcJumpRange(mass, this.standard[2].c, fuel); - }; - - /** - * Find an internal slot that has an installed component of the specific group. - * - * @param {string} group Component group/type - * @return {number} The index of the slot in ship.internal - */ - Ship.prototype.findInternalByGroup = function(group) { - var index; - if (group == 'sg' || group == 'psg' || group == 'bsg') { - index = _.findIndex(this.internal, function(slot) { - return slot.c && (slot.c.grp == 'sg' || slot.c.grp == 'psg' || slot.c.grp == 'bsg'); - }); - } else { - index = _.findIndex(this.internal, function(slot) { - return slot.c && slot.c.grp == group; - }); - } - - if (index !== -1) { - return this.internal[index]; - } - return null; - }; - - //**********************// - // Mutate / Update Ship // - //**********************// - - /** - * Recalculate all item costs and total based on discounts. - * @param {number} shipCostMultiplier Ship cost multiplier discount (e.g. 0.9 === 10% discount) - * @param {number} componentCostMultiplier Component cost multiplier discount (e.g. 0.75 === 25% discount) - */ - Ship.prototype.applyDiscounts = function(shipCostMultiplier, componentCostMultiplier) { - var total = 0; - var costList = this.costList; - - for (var i = 0, l = costList.length; i < l; i++) { - var item = costList[i]; - if (item.c && item.c.cost) { - item.discountedCost = item.c.cost * (item.type == 'SHIP' ? shipCostMultiplier : componentCostMultiplier); - if (item.incCost) { - total += item.discountedCost; - } - } - } - this.shipCostMultiplier = shipCostMultiplier; - this.componentCostMultiplier = componentCostMultiplier; - this.totalCost = total; - return this; - }; - - /** - * Builds/Updates the ship instance with the components[comps] passed in. - * @param {object} comps Collection of components used to build the ship - */ - Ship.prototype.buildWith = function(comps, priorities, enabled) { - var internal = this.internal, - standard = this.standard, - hps = this.hardpoints, - bands = this.priorityBands, - cl = standard.length, - i, l; - - // Reset Cumulative stats - this.fuelCapacity = 0; - this.cargoCapacity = 0; - this.ladenMass = 0; - this.armourAdded = 0; - this.armourMultiplier = 1; - this.shieldMultiplier = 1; - this.totalCost = this.c.incCost ? this.c.discountedCost : 0; - this.unladenMass = this.hullMass; - this.totalDps = 0; - - this.bulkheads.c = null; - this.useBulkhead(comps && comps.bulkheads ? comps.bulkheads : 0, true); - this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; - this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; - - for (i = 0, l = this.priorityBands.length; i < l; i++) { - this.priorityBands[i].deployed = 0; - this.priorityBands[i].retracted = 0; - this.priorityBands[i].retOnly = 0; - } - - if (this.cargoHatch.enabled) { - bands[this.cargoHatch.priority].retracted += this.cargoHatch.c.power; - } - - for (i = 0; i < cl; i++) { - standard[i].cat = 0; - standard[i].enabled = enabled ? enabled[i + 1] * 1 : true; - standard[i].priority = priorities && priorities[i + 1] ? priorities[i + 1] * 1 : 0; - standard[i].type = 'SYS'; - standard[i].c = standard[i].id = null; // Resetting 'old' component if there was one - standard[i].discountedCost = 0; - - if (comps) { - this.use(standard[i], comps.standard[i], Components.standard(i, comps.standard[i]), true); - } - } - - standard[1].type = 'ENG'; // Thrusters - standard[2].type = 'ENG'; // FSD - cl++; // Increase accounts for Cargo Scoop - - for (i = 0, l = hps.length; i < l; i++) { - hps[i].cat = 1; - hps[i].enabled = enabled ? enabled[cl + i] * 1 : true; - hps[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0; - hps[i].type = hps[i].maxClass ? 'WEP' : 'SYS'; - hps[i].c = hps[i].id = null; // Resetting 'old' component if there was one - hps[i].discountedCost = 0; - - if (comps && comps.hardpoints[i] !== 0) { - this.use(hps[i], comps.hardpoints[i], Components.hardpoints(comps.hardpoints[i]), true); - } - } - - cl += hps.length; // Increase accounts for hardpoints - - for (i = 0, l = internal.length; i < l; i++) { - internal[i].cat = 2; - internal[i].enabled = enabled ? enabled[cl + i] * 1 : true; - internal[i].priority = priorities && priorities[cl + i] ? priorities[cl + i] * 1 : 0; - internal[i].type = 'SYS'; - internal[i].id = internal[i].c = null; // Resetting 'old' component if there was one - internal[i].discountedCost = 0; - - if (comps && comps.internal[i] !== 0) { - this.use(internal[i], comps.internal[i], Components.internal(comps.internal[i]), true); - } - } - - // Update aggragated stats - if (comps) { - this.updatePower() - .updateJumpStats() - .updateShieldStrength() - .updateTopSpeed(); - } - - return this; - }; - - Ship.prototype.emptyHardpoints = function() { - for (var i = this.hardpoints.length; i--; ) { - this.use(this.hardpoints[i], null, null); - } - return this; - }; - - Ship.prototype.emptyInternal = function() { - for (var i = this.internal.length; i--; ) { - this.use(this.internal[i], null, null); - } - return this; - }; - - Ship.prototype.emptyUtility = function() { - for (var i = this.hardpoints.length; i--; ) { - if (!this.hardpoints[i].maxClass) { - this.use(this.hardpoints[i], null, null); - } - } - return this; - }; - - Ship.prototype.emptyWeapons = function() { - for (var i = this.hardpoints.length; i--; ) { - if (this.hardpoints[i].maxClass) { - this.use(this.hardpoints[i], null, null); - } - } - return this; - }; - - /** - * Optimize for the lower mass build that can still boost and power the ship - * without power management. - * @param {object} c Standard Component overrides - */ - Ship.prototype.optimizeMass = function(c) { - return this.emptyHardpoints().emptyInternal().useLightestStandard(c); - }; - - Ship.prototype.setCostIncluded = function(item, included) { - if (item.incCost != included && item.c) { - this.totalCost += included ? item.discountedCost : -item.discountedCost; - } - item.incCost = included; - return this; - }; - - Ship.prototype.setSlotEnabled = function(slot, enabled) { - if (slot.enabled != enabled) { // Enabled state is changing - slot.enabled = enabled; - if (slot.c) { - this.priorityBands[slot.priority][powerUsageType(slot, slot.c)] += enabled ? slot.c.power : -slot.c.power; - - if (slot.c.grp == 'sg' || slot.c.grp == 'psg' || slot.c.grp == 'bsg') { - this.updateShieldStrength(); - } else if (slot.c.grp == 'sb') { - this.shieldMultiplier += slot.c.shieldmul * (enabled ? 1 : -1); - this.updateShieldStrength(); - } else if (slot.c.dps) { - this.totalDps += slot.c.dps * (enabled ? 1 : -1); - } - - this.updatePower(); - } - } - return this; - }; - - /** - * Updates the ship's cumulative and aggregated stats based on the component change. - */ - Ship.prototype.updateStats = function(slot, n, old, preventUpdate) { - var powerChange = slot == this.standard[0]; - - if (old) { // Old component now being removed - switch (old.grp) { - case 'ft': - this.fuelCapacity -= old.capacity; - break; - case 'cr': - this.cargoCapacity -= old.capacity; - break; - case 'hr': - this.armourAdded -= old.armouradd; - break; - case 'sb': - this.shieldMultiplier -= slot.enabled ? old.shieldmul : 0; - break; - } - - if (slot.incCost && old.cost) { - this.totalCost -= old.cost * this.componentCostMultiplier; - } - - if (old.power && slot.enabled) { - this.priorityBands[slot.priority][powerUsageType(slot, old)] -= old.power; - powerChange = true; - - if (old.dps) { - this.totalDps -= old.dps; - } - } - this.unladenMass -= old.mass || 0; - } - - if (n) { - switch (n.grp) { - case 'ft': - this.fuelCapacity += n.capacity; - break; - case 'cr': - this.cargoCapacity += n.capacity; - break; - case 'hr': - this.armourAdded += n.armouradd; - break; - case 'sb': - this.shieldMultiplier += slot.enabled ? n.shieldmul : 0; - break; - } - - if (slot.incCost && n.cost) { - this.totalCost += n.cost * this.componentCostMultiplier; - } - - if (n.power && slot.enabled) { - this.priorityBands[slot.priority][powerUsageType(slot, n)] += n.power; - powerChange = true; - - if (n.dps) { - this.totalDps += n.dps; - } - } - this.unladenMass += n.mass || 0; - } - - this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity; - this.armour = this.armourAdded + Math.round(this.baseArmour * this.armourMultiplier); - - if (!preventUpdate) { - if (powerChange) { - this.updatePower(); - } - this.updateTopSpeed(); - this.updateJumpStats(); - this.updateShieldStrength(); - } - return this; - }; - - Ship.prototype.updatePower = function() { - var bands = this.priorityBands; - var prevRetracted = 0, prevDeployed = 0; - - for (var i = 0, l = bands.length; i < l; i++) { - var band = bands[i]; - prevRetracted = band.retractedSum = prevRetracted + band.retracted + band.retOnly; - prevDeployed = band.deployedSum = prevDeployed + band.deployed + band.retracted; - } - - this.powerAvailable = this.standard[0].c.pGen; - this.powerRetracted = prevRetracted; - this.powerDeployed = prevDeployed; - return this; - }; - - Ship.prototype.updateTopSpeed = function() { - var speeds = calcSpeed(this.unladenMass + this.fuelCapacity, this.speed, this.boost, this.standard[1].c, this.pipSpeed); - this.topSpeed = speeds['4 Pips']; - this.topBoost = speeds.boost; - return this; - }; - - Ship.prototype.updateShieldStrength = function() { - var sgSlot = this.findInternalByGroup('sg'); // Find Shield Generator slot Index if any - this.shieldStrength = sgSlot && sgSlot.enabled ? calcShieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.c, this.shieldMultiplier) : 0; - return this; - }; - - /** - * Jump Range and total range calculations - */ - Ship.prototype.updateJumpStats = function() { - var fsd = this.standard[2].c; // Frame Shift Drive; - this.unladenRange = calcJumpRange(this.unladenMass + fsd.maxfuel, fsd, this.fuelCapacity); // Include fuel weight for jump - this.fullTankRange = calcJumpRange(this.unladenMass + this.fuelCapacity, fsd, this.fuelCapacity); // Full Tanke - this.ladenRange = calcJumpRange(this.ladenMass, fsd, this.fuelCapacity); - this.unladenTotalRange = calcTotalRange(this.unladenMass, fsd, this.fuelCapacity); - this.ladenTotalRange = calcTotalRange(this.unladenMass + this.cargoCapacity, fsd, this.fuelCapacity); - this.maxJumpCount = Math.ceil(this.fuelCapacity / fsd.maxfuel); - return this; - }; - - - /** - * Update a slot with a the component if the id is different from the current id for this slot. - * Has logic handling components that you may only have 1 of (Shield Generator or Refinery). - * - * @param {object} slot The component slot - * @param {string} id Unique ID for the selected component - * @param {object} component Properties for the selected component - * @param {boolean} preventUpdate If true, do not update aggregated stats - */ - Ship.prototype.use = function(slot, id, component, preventUpdate) { - if (slot.id != id) { // Selecting a different component - // Slot is an internal slot, is not being emptied, and the selected component group/type must be of unique - if (slot.cat == 2 && component && _.includes(['bsg', 'psg', 'sg', 'rf', 'fs'], component.grp)) { - // Find another internal slot that already has this type/group installed - var similarSlot = this.findInternalByGroup(component.grp); - // If another slot has an installed component with of the same type - if (!preventUpdate && similarSlot && similarSlot !== slot) { - this.updateStats(similarSlot, null, similarSlot.c); - similarSlot.id = similarSlot.c = null; // Empty the slot - similarSlot.discountedCost = 0; - } - } - var oldComponent = slot.c; - slot.id = id; - slot.c = component; - slot.discountedCost = (component && component.cost) ? component.cost * this.componentCostMultiplier : 0; - this.updateStats(slot, component, oldComponent, preventUpdate); - } - return this; - }; - - /** - * [useBulkhead description] - * @param {[type]} index [description] - * @param {[type]} preventUpdate [description] - * @return {[type]} [description] - */ - Ship.prototype.useBulkhead = function(index, preventUpdate) { - var oldBulkhead = this.bulkheads.c; - this.bulkheads.id = index; - this.bulkheads.c = Components.bulkheads(this.id, index); - this.bulkheads.discountedCost = this.bulkheads.c.cost * this.componentCostMultiplier; - this.armourMultiplier = ArmourMultiplier[index]; - this.updateStats(this.bulkheads, this.bulkheads.c, oldBulkhead, preventUpdate); - - return this; - }; - - /** - * [useStandard description] - * @param {[type]} rating [description] - * @return {[type]} [description] - */ - Ship.prototype.useStandard = function(rating) { - for (var i = this.standard.length - 1; i--; ) { // All except Fuel Tank - var id = this.standard[i].maxClass + rating; - this.use(this.standard[i], id, Components.standard(i, id)); - } - return this; - }; - - /** - * Use the lightest standard components unless otherwise specified - * @param {object} c Component overrides - */ - Ship.prototype.useLightestStandard = function(c) { - c = c || {}; - - var standard = this.standard, - pd = c.pd || this.availCS.lightestPowerDist(this.boostEnergy), // Find lightest Power Distributor that can still boost; - fsd = c.fsd || standard[2].maxClass + 'A', - ls = c.ls || standard[3].maxClass + 'D', - s = c.s || standard[5].maxClass + 'D', - updated; - - this.useBulkhead(0) - .use(standard[2], fsd, Components.standard(2, fsd)) // FSD - .use(standard[3], ls, Components.standard(3, ls)) // Life Support - .use(standard[5], s, Components.standard(5, s)) // Sensors - .use(standard[4], pd, Components.standard(4, pd)); // Power Distributor - - // Thrusters and Powerplant must be determined after all other components are mounted - // Loop at least once to determine absolute lightest PD and TH - do { - updated = false; - // Find lightest Thruster that still works for the ship at max mass - var th = c.th || this.availCS.lightestThruster(this.ladenMass); - if (th != standard[1].id) { - this.use(standard[1], th, Components.standard(1, th)); - updated = true; - } - // Find lightest Power plant that can power the ship - var pp = c.pp || this.availCS.lightestPowerPlant(Math.max(this.powerRetracted, this.powerDeployed), c.ppRating); - - if (pp != standard[0].id) { - this.use(standard[0], pp, Components.standard(0, pp)); - updated = true; - } - } while (updated); - - return this; - }; - - Ship.prototype.useUtility = function(group, rating, name, clobber) { - var component = Components.findHardpoint(group, 0, rating, name); - for (var i = this.hardpoints.length; i--; ) { - if ((clobber || !this.hardpoints[i].c) && !this.hardpoints[i].maxClass) { - this.use(this.hardpoints[i], component.id, component); - } - } - return this; - }; - - Ship.prototype.useWeapon = function(group, mount, clobber, missile) { - var hps = this.hardpoints; - for (var i = hps.length; i--; ) { - if (hps[i].maxClass) { - var size = hps[i].maxClass, component; - do { - component = Components.findHardpoint(group, size, null, null, mount, missile); - if ((clobber || !hps[i].c) && component) { - this.use(hps[i], component.id, component); - break; - } - } while (!component && (--size > 0)); - } - } - return this; - }; - - /** - * Will change the priority of the specified slot if the new priority is valid - * @param {object} slot The slot to be updated - * @param {number} newPriority The new priority to be set - * @return {boolean} Returns true if the priority was changed (within range) - */ - Ship.prototype.changePriority = function(slot, newPriority) { - if (newPriority >= 0 && newPriority < this.priorityBands.length) { - var oldPriority = slot.priority; - slot.priority = newPriority; - - if (slot.enabled) { // Only update power if the slot is enabled - var usage = powerUsageType(slot, slot.c); - this.priorityBands[oldPriority][usage] -= slot.c.power; - this.priorityBands[newPriority][usage] += slot.c.power; - this.updatePower(); - } - return true; - } - return false; - }; - - return Ship; -}]); diff --git a/app/js/shipyard/module-shipyard.js b/app/js/shipyard/module-shipyard.js deleted file mode 100755 index 7fd6e5f5..00000000 --- a/app/js/shipyard/module-shipyard.js +++ /dev/null @@ -1,255 +0,0 @@ -/** - * This module contains all of the logic and models corresponding to - * information or behavoir in Elite Dangerous. - * - * This file contains values and functions that can be reused across the app. - * - * @requires ngLodash - */ -angular.module('shipyard', ['ngLodash']) - // Create 'angularized' references to DB. This will aid testing - .constant('ShipsDB', DB.ships) - .constant('ComponentsDB', DB.components) - .constant('ArmourMultiplier', [ - 1, // Lightweight - 1.4, // Reinforced - 1.945, // Military - 1.945, // Mirrored - 1.945 // Reactive - ]) - .constant('SizeMap', ['', 'small', 'medium', 'large', 'capital']) - // Map to lookup group labels/names for component grp, used for JSON Serialization - .constant('GroupMap', { - // Standard - pp: 'Power Plant', - t: 'Thrusters', - fsd: 'Frame Shift Drive', - ls: 'Life Support', - pd: 'Power Distributor', - s: 'Sensors', - ft: 'Fuel Tank', - - // Internal - fs: 'Fuel Scoop', - sc: 'Scanner', - am: 'Auto Field-Maintenance Unit', - cr: 'Cargo Rack', - fi: 'Frame Shift Drive Interdictor', - hb: 'Hatch Breaker Limpet Controller', - hr: 'Hull Reinforcement Package', - rf: 'Refinery', - scb: 'Shield Cell Bank', - sg: 'Shield Generator', - psg: 'Prismatic Shield Generator', - bsg: 'Bi-Weave Shield Generator', - dc: 'Docking Computer', - fx: 'Fuel Transfer Limpet Controller', - pc: 'Prospector Limpet Controller', - cc: 'Collector Limpet Controller', - pv: 'Planetary Vehicle Hangar', - - // Hard Points - bl: 'Beam Laser', - ul: 'Burst Laser', - c: 'Cannon', - cs: 'Cargo Scanner', - cm: 'Countermeasure', - fc: 'Fragment Cannon', - ws: 'Frame Shift Wake Scanner', - kw: 'Kill Warrant Scanner', - nl: 'Mine Launcher', - ml: 'Mining Laser', - mr: 'Missile Rack', - pa: 'Plasma Accelerator', - mc: 'Multi-cannon', - pl: 'Pulse Laser', - rg: 'Rail Gun', - sb: 'Shield Booster', - tp: 'Torpedo Pylon' - }) - .constant('MountMap', { - 'F': 'Fixed', - 'G': 'Gimballed', - 'T': 'Turret', - 'Fixed': 'F', - 'Gimballed': 'G', - 'Turret': 'T' - }) - /** - * Array of all Ship properties (facets) organized into groups - * used for ship comparisons. - * - * @type {Array} - */ - .constant('ShipFacets', [ - { // 0 - title: 'agility', - props: ['agility'], - unit: '', - fmt: 'fCrd' - }, - { // 1 - title: 'speed', - props: ['topSpeed', 'topBoost'], - lbls: ['thrusters', 'boost'], - unit: 'm/s', - fmt: 'fCrd' - }, - { // 2 - title: 'armour', - props: ['armour'], - unit: '', - fmt: 'fCrd' - }, - { // 3 - title: 'shields', - props: ['shieldStrength'], - unit: 'MJ', - fmt: 'fRound' - }, - { // 4 - title: 'jump range', - props: ['unladenRange', 'fullTankRange', 'ladenRange'], - lbls: ['max', 'full tank', 'laden'], - unit: 'LY', - fmt: 'fRound' - }, - { // 5 - title: 'mass', - props: ['unladenMass', 'ladenMass'], - lbls: ['unladen', 'laden'], - unit: 'T', - fmt: 'fRound' - }, - { // 6 - title: 'cargo', - props: ['cargoCapacity'], - unit: 'T', - fmt: 'fRound' - }, - { // 7 - title: 'fuel', - props: ['fuelCapacity'], - unit: 'T', - fmt: 'fRound' - }, - { // 8 - title: 'power', - props: ['powerRetracted', 'powerDeployed', 'powerAvailable'], - lbls: ['retracted', 'deployed', 'available'], - unit: 'MW', - fmt: 'fPwr' - }, - { // 9 - title: 'cost', - props: ['totalCost'], - unit: 'CR', - fmt: 'fCrd' - }, - { // 10 - title: 'total range', - props: ['unladenTotalRange', 'ladenTotalRange'], - lbls: ['unladen', 'laden'], - unit: 'LY', - fmt: 'fRound' - }, - { // 11 - title: 'DPS', - props: ['totalDps'], - lbls: ['DPS'], - unit: '', - fmt: 'fRound' - } - ]) - /** - * Set of all available / theoretical discounts - */ - .constant('Discounts', { - '0%': 1, - '2.5%': 0.975, - '5%': 0.95, - '10%': 0.90, - '15%': 0.85, - '20%': 0.80, - '25%': 0.75 - }) - /** - * Calculate the maximum single jump range based on mass and a specific FSD - * - * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc - * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass - * @param {number} fuel Optional - The fuel consumed during the jump (must be less than the drives max fuel per jump) - * @return {number} Distance in Light Years - */ - .value('calcJumpRange', function(mass, fsd, fuel) { - return Math.pow(Math.min(fuel === undefined ? fsd.maxfuel : fuel, fsd.maxfuel) / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass; - }) - /** - * Calculate the total range based on mass and a specific FSD, and all fuel available - * - * @param {number} mass Mass of a ship: laden, unlanden, partially laden, etc - * @param {object} fsd The FDS object/component with maxfuel, fuelmul, fuelpower, optmass - * @param {number} fuel The total fuel available - * @return {number} Distance in Light Years - */ - .value('calcTotalRange', function(mass, fsd, fuel) { - var fuelRemaining = fuel % fsd.maxfuel; // Fuel left after making N max jumps - var jumps = Math.floor(fuel / fsd.maxfuel); - mass += fuelRemaining; - // Going backwards, start with the last jump using the remaining fuel - var totalRange = fuelRemaining > 0 ? Math.pow(fuelRemaining / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass : 0; - // For each max fuel jump, calculate the max jump range based on fuel mass left in the tank - for (var j = 0; j < jumps; j++) { - mass += fsd.maxfuel; - totalRange += Math.pow(fsd.maxfuel / fsd.fuelmul, 1 / fsd.fuelpower ) * fsd.optmass / mass; - } - return totalRange; - }) - /** - * Calculate the a ships shield strength based on mass, shield generator and shield boosters used. - * - * @param {number} mass Current mass of the ship - * @param {number} shields Base Shield strength MJ for ship - * @param {object} sg The shield generator used - * @param {number} multiplier Shield multiplier for ship (1 + shield boosters if any) - * @return {number} Approximate shield strengh in MJ - */ - .value('calcShieldStrength', function(mass, shields, sg, multiplier) { - var opt; - if (mass < sg.minmass) { - return shields * multiplier * sg.minmul; - } - if (mass > sg.maxmass) { - return shields * multiplier * sg.maxmul; - } - if (mass < sg.optmass) { - opt = (sg.optmass - mass) / (sg.optmass - sg.minmass); - opt = 1 - Math.pow(1 - opt, 0.87); - return shields * multiplier * ((opt * sg.minmul) + ((1 - opt) * sg.optmul)); - } else { - opt = (sg.optmass - mass) / (sg.maxmass - sg.optmass); - opt = -1 + Math.pow(1 + opt, 2.425); - return shields * multiplier * ( (-1 * opt * sg.maxmul) + ((1 + opt) * sg.optmul) ); - } - }) - /** - * Calculate the a ships speed based on mass, and thrusters. - * - * @param {number} mass Current mass of the ship - * @param {number} baseSpeed Base speed m/s for ship - * @param {number} baseBoost Base boost speed m/s for ship - * @param {object} thrusters The Thrusters used - * @param {number} pipSpeed Speed pip multiplier - * @return {object} Approximate speed by pips - */ - .value('calcSpeed', function(mass, baseSpeed, baseBoost, thrusters, pipSpeed) { - var multiplier = mass > thrusters.maxmass ? 0 : ((1 - thrusters.M) + (thrusters.M * Math.pow(3 - (2 * Math.max(0.5, mass / thrusters.optmass)), thrusters.P))); - var speed = baseSpeed * multiplier; - - return { - '0 Pips': speed * (1 - (pipSpeed * 4)), - '2 Pips': speed * (1 - (pipSpeed * 2)), - '4 Pips': speed, - 'boost': baseBoost * multiplier - }; - }); diff --git a/app/js/shipyard/service-components.js b/app/js/shipyard/service-components.js deleted file mode 100755 index f4c540d6..00000000 --- a/app/js/shipyard/service-components.js +++ /dev/null @@ -1,181 +0,0 @@ -angular.module('shipyard').service('Components', ['lodash', 'ComponentsDB', 'ShipsDB', 'ComponentSet', 'GroupMap', function(_, C, Ships, ComponentSet, GroupMap) { - - var GrpNameToCodeMap = {}; - - for (var grp in GroupMap) { - GrpNameToCodeMap[GroupMap[grp]] = grp; - } - - this.cargoHatch = function() { - return { name: 'Cargo Hatch', class: 1, rating: 'H', power: 0.6 }; - }; - - this.standard = function(typeIndex, componentId) { - return C.standard[typeIndex][componentId]; - }; - - this.hardpoints = function(id) { - for (var n in C.hardpoints) { - var group = C.hardpoints[n]; - for (var i = 0; i < group.length; i++) { - if (group[i].id == id) { - return group[i]; - } - } - } - return null; - }; - - this.internal = function(id) { - for (var n in C.internal) { - var group = C.internal[n]; - for (var i = 0; i < group.length; i++) { - if (group[i].id == id) { - return group[i]; - } - } - } - return null; - }; - - /** - * Finds an internal Component based on Class, Rating, Group and/or name. - * At least one ofGroup name or unique component name must be provided - * - * @param {string} groupName [Optional] Full name or abbreviated name for component group - * @param {integer} clss Component Class - * @param {string} rating Component Rating - * @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner' - * @return {String} The id of the component if found, null if not found - */ - this.findInternal = function(groupName, clss, rating, name) { - var groups = {}; - - if (groupName) { - if (C.internal[groupName]) { - groups[groupName] = C.internal[groupName]; - } else { - var grpCode = GrpNameToCodeMap[groupName]; - if (grpCode && C.internal[grpCode]) { - groups[grpCode] = C.internal[grpCode]; - } - } - } else if (name) { - groups = C.internal; - } - - for (var g in groups) { - var group = groups[g]; - for (var i = 0, l = group.length; i < l; i++) { - if (group[i].class == clss && group[i].rating == rating && ((!name && !group[i].name) || group[i].name == name)) { - return group[i]; - } - } - } - - return null; - }; - - /** - * Finds an internal Component ID based on Class, Rating, Group and/or name. - * At least one ofGroup name or unique component name must be provided - * - * @param {string} groupName [Optional] Full name or abbreviated name for component group - * @param {integer} clss Component Class - * @param {string} rating Component Rating - * @param {string} name [Optional] Long/unique name for component -e.g. 'Advanced Discover Scanner' - * @return {String} The id of the component if found, null if not found - */ - this.findInternalId = function(groupName, clss, rating, name) { - var i = this.findInternal(groupName, clss, rating, name); - return i ? i.id : 0; - }; - - /** - * Finds a hardpoint Component based on Class, Rating, Group and/or name. - * At least one ofGroup name or unique component name must be provided - * - * @param {string} groupName [Optional] Full name or abbreviated name for component group - * @param {integer} clss Component Class - * @param {string} rating [Optional] Component Rating - * @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher' - * @param {string} mode Mount mode/type - [F]ixed, [G]imballed, [T]urret - * @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker - * @return {String} The id of the component if found, null if not found - */ - this.findHardpoint = function(groupName, clss, rating, name, mode, missile) { - var groups = {}; - - if (groupName) { - if (C.hardpoints[groupName]) { - groups[groupName] = C.hardpoints[groupName]; - } else { - var grpCode = GrpNameToCodeMap[groupName]; - if (grpCode && C.hardpoints[grpCode]) { - groups[grpCode] = C.hardpoints[grpCode]; - } - } - } else if (name) { - groups = C.hardpoints; - } - - for (var g in groups) { - var group = groups[g]; - for (var i = 0, l = group.length; i < l; i++) { - if (group[i].class == clss && (!rating || group[i].rating == rating) && group[i].mode == mode - && ((!name && !group[i].name) || group[i].name == name) - && ((!missile && !group[i].missile) || group[i].missile == missile) - ) { - return group[i]; - } - } - } - - return null; - }; - - /** - * Finds a hardpoint Component ID based on Class, Rating, Group and/or name. - * At least one of Group name or unique component name must be provided - * - * @param {string} groupName [Optional] Full name or abbreviated name for component group - * @param {integer} clss Component Class - * @param {string} rating Component Rating - * @param {string} name [Optional] Long/unique name for component -e.g. 'Heat Sink Launcher' - * @param {string} mode Mount mode/type - [F]ixed, [G]imballed, [T]urret - * @param {string} missile [Optional] Missile type - [D]umbfire, [S]eeker - * @return {String} The id of the component if found, null if not found - */ - this.findHardpointId = function(groupName, clss, rating, name, mode, missile) { - var h = this.findHardpoint(groupName, clss, rating, name, mode, missile); - return h ? h.id : 0; - }; - - /** - * Looks up the bulkhead component for a specific ship and bulkhead - * @param {string} shipId Unique ship Id/Key - * @param {number} bulkheadsId Id/Index for the specified bulkhead - * @return {object} The bulkhead component object - */ - this.bulkheads = function(shipId, bulkheadsId) { - return C.bulkheads[shipId][bulkheadsId]; - }; - - this.bulkheadIndex = function(bulkheadName) { - return ['Lightweight Alloy', 'Reinforced Alloy', 'Military Grade Composite', 'Mirrored Surface Composite', 'Reactive Surface Composite'].indexOf(bulkheadName); - }; - - /** - * Creates a new ComponentSet that contains all available components - * that the specified ship is eligible to use. - * - * @param {string} shipId Unique ship Id/Key - * @return {ComponentSet} The set of components the ship can install - */ - this.forShip = function(shipId) { - var ship = Ships[shipId]; - var maxInternal = isNaN(ship.slots.internal[0]) ? ship.slots.internal[0].class : ship.slots.internal[0]; - return new ComponentSet(C, ship.minMassFilter || ship.properties.hullMass + 5, ship.slots.standard, maxInternal, ship.slots.hardpoints[0]); - }; - -}]); diff --git a/app/less/background-images.less b/app/less/background-images.less deleted file mode 100755 index 295dd50e..00000000 --- a/app/less/background-images.less +++ /dev/null @@ -1,8 +0,0 @@ - -.deep-space { - background-image: url(images/deep-space-1920x1080.jpg); -} - -.docking-bay { - background-image: url(images/bay-1920x1080.jpg); -} \ No newline at end of file diff --git a/app/less/chart-tooltip.less b/app/less/chart-tooltip.less deleted file mode 100755 index 04479a62..00000000 --- a/app/less/chart-tooltip.less +++ /dev/null @@ -1,63 +0,0 @@ -.d3-tip { - font-size: 0.8em; - padding: 0.25em 0.5em; - background: @primary-disabled; - text-transform: capitalize; - color: @primary-bg; - pointer-events: none; -} - -/* Creates a small triangle extender for the tooltip */ -.d3-tip:after { - box-sizing: border-box; - display: inline; - font-size: 10px; - width: 100%; - line-height: 1; - color: @primary-disabled; - position: absolute; - pointer-events: none; -} - -/* Northward tooltips */ -.d3-tip.n { - margin-top: -7px; - &:after { - content: "\25BC"; - margin: -1px 0 0 0; - top: 100%; - left: 0; - text-align: center; - } -} - -/* Eastward tooltips */ -.d3-tip.e { - margin-left: 8px; - &:after { - content: "\25C0"; - margin: -4px 0 0 0; - top: 50%; - left: -8px; - } -} - -/* Southward tooltips */ -.d3-tip.s { - margin-top: 8px; - &:after { - content: "\25B2"; - margin: 0 0 1px 0; - top: -7px; - left: 0; - text-align: center; - } -} - -/* Westward tooltips */ -.d3-tip.w:after { - content: "\25B6"; - margin: -4px 0 0 -1px; - top: 50%; - left: 100%; -} \ No newline at end of file diff --git a/app/less/fonts.less b/app/less/fonts.less deleted file mode 100755 index f8c0b0f4..00000000 --- a/app/less/fonts.less +++ /dev/null @@ -1,26 +0,0 @@ -@font-face { - font-family: 'Orbitron-Regular'; - src: url('fonts/orbitron-regular-webfont.eot'); - src: url('fonts/orbitron-regular-webfont.eot?#iefix') format('embedded-opentype'), - url('fonts/orbitron-regular-webfont.woff2') format('woff2'), - url('fonts/orbitron-regular-webfont.woff') format('woff'), - url('fonts/orbitron-regular-webfont.ttf') format('truetype'), - url('fonts/orbitron-regular-webfont.svg#orbitronregular') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Eurostile'; - src: url('fonts/eurostile.eot'); - src: url('fonts/eurostile.eot?#iefix') format('embedded-opentype'), - url('fonts/eurostile.woff2') format('woff2'), - url('fonts/eurostile.woff') format('woff'), - url('fonts/eurostile.ttf') format('truetype'), - url('fonts/eurostile.svg#euro_capsregular') format('svg'); - font-weight: normal; - font-style: normal; -} - -@fStandard: 'Eurostile', Helvetica, sans-serif; -@fTitle: 'Orbitron-Regular', Arial, sans-serif; diff --git a/app/less/slider.less b/app/less/slider.less deleted file mode 100644 index 43d19a87..00000000 --- a/app/less/slider.less +++ /dev/null @@ -1,98 +0,0 @@ - -.slider-axis { - line, path { - fill: none; - stroke: @primary-disabled; - } - - text { - font-size: 0.7em; - fill: @primary-disabled; - } - - - .domain { - fill: none; - stroke: @primary; - stroke-opacity: .3; - stroke-width: 0.7em; - stroke-linecap: round; - } - -} - -.slider { - - text { - dominant-baseline: central; - fill: @primary; - font-size: 0.8em; - } - - .filled { - stroke-width: 0.3em; - stroke-linecap: round; - stroke: @primary-disabled; - } - - .handle { - fill: @primary; - stroke-opacity: .5; - cursor: crosshair; - } -} - -input[type=range] { - -webkit-appearance: none; - border: 1px solid @bgBlack; - /*required for proper track sizing in FF*/ - width: 300px; - - &::-moz-range-track, &::-webkit-slider-runnable-track { - width: 300px; - height: 5px; - background: @primary; - border: none; - border-radius: 3px; - } - &::-moz-range-thumb, &::-webkit-slider-thumb { - -webkit-appearance: none; - border: none; - height: 1em; - width: 1em; - border-radius: 50%; - background: @primary; - } - &:focus { - outline: none; - } - /*hide the outline behind the border*/ - &:-moz-focusring{ - outline: 1px solid @bgBlack; - outline-offset: -1px; - } - - &::-ms-track { - width: 300px; - height: 5px; - background: transparent; - border-color: transparent; - border-width: 6px 0; - color: transparent; - } - &::-ms-fill-lower { - background: @primary; - border-radius: 10px; - } - &::-ms-fill-upper { - background: @primary; - border-radius: 10px; - } - &::-ms-thumb { - border: none; - height: 16px; - width: 16px; - border-radius: 50%; - background: goldenrod; - } -} diff --git a/app/less/sortable.less b/app/less/sortable.less deleted file mode 100755 index b5794c70..00000000 --- a/app/less/sortable.less +++ /dev/null @@ -1,30 +0,0 @@ -.sortable { - .user-select-none(); -} - -.as-sortable-item, .as-sortable-placeholder { - display: inline-block; - float: left; -} - -.as-sortable-item { - -ms-touch-action: none; - touch-action: none; -} - -.as-sortable-item-handle { - display: block; -} - -.as-sortable-drag { - margin: 0; - padding:0; - opacity: .8; - position: absolute; - pointer-events: none; - z-index: 9999; -} - -.as-sortable-hidden { - display: none !important; -} diff --git a/app/views/_header.html b/app/views/_header.html deleted file mode 100755 index 0a9abc64..00000000 --- a/app/views/_header.html +++ /dev/null @@ -1,91 +0,0 @@ -
    - {{ 'PHRASE_UPDATE_RDY' | translate }} -
    - -
    - - - - - - - - - - -
    \ No newline at end of file diff --git a/app/views/_modal.html b/app/views/_modal.html deleted file mode 100755 index 946cf418..00000000 --- a/app/views/_modal.html +++ /dev/null @@ -1,3 +0,0 @@ - \ No newline at end of file diff --git a/app/views/_slot-hardpoint.html b/app/views/_slot-hardpoint.html deleted file mode 100755 index 14d211fc..00000000 --- a/app/views/_slot-hardpoint.html +++ /dev/null @@ -1,17 +0,0 @@ -
    {{['U','S','M','L','H'][hp.maxClass] | translate}}
    -
    -
    - {{hp.c.class}}{{hp.c.rating}}/{{hp.c.mode}}{{hp.c.missile}} {{hp.c.name || hp.c.grp | translate}} -
    {{hp.c.mass}} T
    -
    -
    {{'damage' | translate}}: {{hp.c.damage}} ({{$r.fCrd(hp.c.ssdam)}} MJ)
    -
    {{'DPS' | translate}}: {{hp.c.dps}} ({{$r.fCrd(hp.c.mjdps)}} MJ)
    -
    {{'T_LOAD' | translate}}: {{hp.c.thermload}}
    -
    {{'type' | translate}}: {{hp.c.type}}
    -
    {{'ROF' | translate}}: {{hp.c.rof}}/s
    -
    {{'pen' | translate}}: {{hp.c.armourpen}}
    -
    +{{$r.fRPct(hp.c.shieldmul)}}
    -
    {{hp.c.range}} km
    -
    {{'ammo' | translate}}: {{$r.fCrd(hp.c.clip)}}+{{$r.fCrd(hp.c.ammo)}}
    -
    -
    diff --git a/app/views/_slot-internal.html b/app/views/_slot-internal.html deleted file mode 100755 index 19002ce9..00000000 --- a/app/views/_slot-internal.html +++ /dev/null @@ -1,23 +0,0 @@ -
    -
    -
    -
    {{c.c.class}}{{c.c.rating}} {{c.c.name || c.c.grp | translate}}
    -
    {{c.c.mass || c.c.capacity || '0'}}
    -
    -
    {{'optimal mass' | translate}}: {{c.c.optmass}}
    -
    {{'max mass' | translate}}: {{c.c.maxmass}}
    -
    {{c.c.bins}}
    -
    {{'rate' | translate}}: {{c.c.rate}} kg/s   {{'refuel time' | translate}}: {{$r.fTime(fuel * 1000 / c.c.rate)}}
    -
    {{'bays' | translate}}: {{c.c.bays}}
    -
    {{'ammo' | translate}}: {{$r.fCrd(c.c.ammo)}}
    -
    {{'cells' | translate}}: {{c.c.cells}}
    -
    {{'recharge' | translate}}: {{c.c.recharge}} MJ   {{'total' | translate}}: {{c.c.cells * c.c.recharge}} MJ
    -
    {{'repair' | translate}}: {{c.c.repair}}
    -
    {{'range' | translate}} {{c.c.range}} km
    -
    {{'time' | translate}}: {{$r.fTime(c.c.time)}}
    -
    {{'max' | translate}}: {{(c.c.maximum)}}
    -
    {{c.c.rangeLS}}
    -
    -
    {{'range' | translate}}: {{c.c.rangeRating}}
    -
    +{{c.c.armouradd}}
    -
    diff --git a/app/views/modal-about.html b/app/views/modal-about.html deleted file mode 100755 index 5e03eb47..00000000 --- a/app/views/modal-about.html +++ /dev/null @@ -1,29 +0,0 @@ -

    Coriolis

    -

    The Coriolis project was inspired by E:D Shipyard and, of course, -Elite Dangerous. The ultimate goal of Coriolis is to provide rich features to support in-game play and planning while engaging the E:D community to support its development.

    - -

    Coriolis was created using assets and imagery from Elite: Dangerous, with the permission of Frontier Developments plc, for non-commercial purposes. It is not endorsed by nor reflects the views or opinions of Frontier Developments.

    - -
    - - - -

    Github

    - github.com/cmmcleod/coriolis -
    -

    - Coriolis is an open source project. Checkout the list of upcoming features and to-do list on github. - Any and all contributions and feedback are welcome. If you encounter any bugs please report them and provide as much detail as possible. -

    - -
    - - - - -
    - -

    Help keep the lights on! Donations will be used to cover costs of running and maintaining Coriolis. Thanks for helping!

    - - \ No newline at end of file diff --git a/app/views/modal-delete.html b/app/views/modal-delete.html deleted file mode 100755 index f132277b..00000000 --- a/app/views/modal-delete.html +++ /dev/null @@ -1,4 +0,0 @@ -

    -

    - - \ No newline at end of file diff --git a/app/views/modal-export.html b/app/views/modal-export.html deleted file mode 100755 index 2828f7e1..00000000 --- a/app/views/modal-export.html +++ /dev/null @@ -1,6 +0,0 @@ -

    -
    -
    - -
    - \ No newline at end of file diff --git a/app/views/modal-import.html b/app/views/modal-import.html deleted file mode 100755 index fe92aace..00000000 --- a/app/views/modal-import.html +++ /dev/null @@ -1,46 +0,0 @@ -

    -
    - - -
    {{errorMsg}}
    -
    - -
    - - - - - - - - - - - - - - - -
    {{ships[shipId].properties.name}} - - -
    - - - - - - - - - -
    - - -
    - - - -
    - - \ No newline at end of file diff --git a/app/views/modal-link.html b/app/views/modal-link.html deleted file mode 100755 index 726033c2..00000000 --- a/app/views/modal-link.html +++ /dev/null @@ -1,9 +0,0 @@ -

    -
    -

    - -

    -

    - -

    - diff --git a/app/views/page-comparison.html b/app/views/page-comparison.html deleted file mode 100755 index d6ade87f..00000000 --- a/app/views/page-comparison.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - -
    - - - - - - -
    -

    - -
    -
      -
    • -
      -
    • -
    -
    - -
    -
    -
    - -
    -

    {{f.title | translate}}

    -
    - - \ No newline at end of file diff --git a/app/views/page-error.html b/app/views/page-error.html deleted file mode 100755 index e1c47d9b..00000000 --- a/app/views/page-error.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -

    - {{msgPre}} - {{msgHighlight}} - {{msgPost}} -

    - -
    -
    - Create an issue on Github - if this keeps happening. Add these details -
    -
    -
    Browser:
    {{browser}}
    -
    Path:
    {{path}}
    -
    Error:
    {{type}}
    -
    Message:
    {{errorMessage}}
    -
    Details:
    {{details}}
    -
    -
    -
    diff --git a/app/views/page-outfit.html b/app/views/page-outfit.html deleted file mode 100644 index ebb44d1a..00000000 --- a/app/views/page-outfit.html +++ /dev/null @@ -1,503 +0,0 @@ -
    - -
    -

    -
    - - - - - - -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{ship.agility}}/10 - {{fCrd(ship.topSpeed)}} m/s - 0 - - {{fCrd(ship.topBoost)}} m/s - 0 - - - {{fRound(ship.totalDps)}} - {{fCrd(ship.armour)}} - - ({{fRPct(ship.armourMultiplier)}} + {{ship.armourAdded}}) - - {{fCrd(ship.shieldStrength)}} MJ ({{fRPct(ship.shieldMultiplier)}}){{ship.hullMass}} T{{fRound(ship.unladenMass)}} T{{fRound(ship.ladenMass)}} T{{fRound(ship.cargoCapacity)}} T{{fRound(ship.fuelCapacity)}} T{{fRound(ship.unladenRange)}} LY{{fRound(ship.fullTankRange)}} LY{{fRound(ship.ladenRange)}} LY{{fRound(ship.maxJumpCount)}}{{fRound(ship.unladenTotalRange)}} LY{{fRound(ship.ladenTotalRange)}} LY
    -
    - -
    -
    -

    - {{'standard' | translate}} - -

    -
    -
      -
    • -
    • E
    • -
    • D
    • -
    • C
    • -
    • B
    • -
    • A
    • -
    -
    -
      -
    • -
    • -
    -
    -
    -
    -
    -
    8
    -
    -
    {{ship.bulkheads.c.mass}} T
    -
    {{ship.bulkheads.c.name | translate}}
    -
    -
    -
      -
    • -
    • -
    • -
    • -
    • -
    -
    -
    -
    -
    -
    {{::pp.maxClass}}
    -
    {{pp.id}} {{'pp' | translate}}
    -
    {{pp.c.mass}} T
    -
    -
    {{'efficiency' | translate}}: {{pp.c.eff}}
    -
    {{'power' | translate}}: {{pp.c.pGen}} MW
    -
    -
    -
    -
    -
    -
    {{::th.maxClass}}
    -
    {{th.id}} {{'t' | translate}}
    -
    {{th.c.mass}} T
    -
    -
    {{'optimal mass' | translate}}: {{th.c.optmass}} T
    -
    {{'max mass' | translate}}: {{th.c.maxmass}} T
    -
    -
    -
    -
    -
    -
    {{::fsd.maxClass}}
    -
    {{fsd.id}} {{'fd' | translate}}
    -
    {{fsd.c.mass}} T
    -
    -
    {{'optimal mass' | translate}}: {{fsd.c.optmass}} T
    -
    {{'max' | translate}} {{'fuel' | translate}}: {{fsd.c.maxfuel}} T
    -
    -
    -
    -
    -
    -
    {{::ls.maxClass}}
    -
    {{ls.id}} {{'ls' | translate}}
    -
    {{ls.c.mass}} T
    -
    -
    {{'time' | translate}}: {{fTime(ls.c.time)}}
    -
    -
    -
    -
    -
    -
    {{::pd.maxClass}}
    -
    {{pd.id}} {{'pd' | translate}}
    -
    {{pd.c.mass}} T
    -
    -
    {{'WEP' | translate}}: {{pd.c.weaponcapacity}} MJ / {{pd.c.weaponrecharge}} MW
    -
    {{'SYS' | translate}}: {{pd.c.systemcapacity}} MJ / {{pd.c.systemrecharge}} MW
    -
    {{'ENG' | translate}}: {{pd.c.enginecapacity}} MJ / {{pd.c.enginerecharge}} MW
    -
    -
    -
    -
    -
    -
    {{::ss.maxClass}}
    -
    {{ss.id}} {{'s' | translate}}
    -
    {{ss.c.mass}} T
    -
    -
    {{'range' | translate}}: {{ss.c.range}} km
    -
    -
    -
    -
    -
    -
    {{::ft.maxClass}}
    -
    {{ft.id}} {{'ft' | translate}}
    -
    {{ft.c.capacity}} T
    -
    -
    -
    -
    - -
    -
    -

    - {{'internal compartments' | translate}} - -

    -
    -
      -
    • -
    • -
    • -
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -

    - {{'hardpoints' | translate}} - -

    -
    -
      -
    • -
    -
    -
      -
    • -
    • -
    • -
    -
    -
      -
    • -
    • -
    • -
    -
    -
      -
    • -
    • -
    • -
    -
    -
      -
    • -
    • -
    • -
    -
    -
      -
    • -
    • -
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -

    - {{'utility mounts' | translate}} - -

    -
    -
      -
    • -
    -
    -
      -
    • E
    • -
    • D
    • -
    • C
    • -
    • B
    • -
    • A
    • -
    -
    -
      -
    • -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{pp.c.class}}{{pp.c.rating}}1{{fPwr(pp.c.pGen)}}100%

    {{c.c.class}}{{c.c.rating}} {{c.priority + 1}} {{fPwr(c.c.power)}}{{f1Pct(c.c.power/ship.powerAvailable)}} - {{'on' | translate}} - {{'off' | translate}} - - - {{'on' | translate}} - {{'off' | translate}} - -
    -
    -
    - -
    - - - - - - - - -
    - -
    - - - - - - - - - - - - - - -
    - {{'component' | translate}} - [{{'ship' | translate }} -{{fRPct(1 - discounts.ship)}}] - [{{'components' | translate}} -{{fRPct(1 - discounts.components)}}] -
    {{item.c.class}}{{item.c.rating}}{{cName(item)}}{{fCrd(item.discountedCost)}} CR
    - - - - - - - - - -
    {{fCrd(ship.totalCost)}} CR
    {{fCrd(ship.totalCost * insurance.current.pct)}} CR
    -
    - -
    -
    - - - - - - - - - - - - - - - - - - - - -
    - {{'net cost' | translate}} [-{{fRPct(1 - discounts.components)}}] -
    {{item.sellClassRating}}{{item.sellName | translate}}{{item.buyClassRating}}{{item.buyName | translate}}{{ fCrd(item.netCost)}} CR
    -
    - - - - - - - - - - -
    {{fCrd(retrofitTotal)}} CR
    - - -
    -
    - -
    -
    - - - - - - - - - - - - - - - - - - -
    - {{'total cost' | translate}} -[{{fRPct(1 - discounts.ammo)}}] -
    {{item.ammoClassRating}}{{item.ammoName | translate}}{{fCrd(item.ammoMax)}}{{fCrd(item.ammoUnitCost)}} CR{{fCrd(item.ammoTotalCost)}} CR
    -
    - - - - - -
    {{fCrd(ammoTotal)}} CR
    -
    -
    - -
    -

    -
    -
    - -
    -

    -
    -
    - -
    -

    -
    -
    - - -
    -
    - -
    -
    - -
    diff --git a/app/views/page-shipyard.html b/app/views/page-shipyard.html deleted file mode 100755 index 18f18228..00000000 --- a/app/views/page-shipyard.html +++ /dev/null @@ -1,89 +0,0 @@ -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {{fCrd(s.speed)}} m/s{{fCrd(s.boost)}} m/s{{fCrd(s.baseShieldStrength)}} Mj{{fCrd(s.topSpeed)}} m/s{{fCrd(s.topBoost)}} m/s{{fRound(s.maxJumpRange)}} LY{{fCrd(s.maxCargo)}} T{{fCrd(s.hullMass)}} T{{fCrd(s.retailCost)}} CR
    -
    - - -
    \ No newline at end of file diff --git a/bower.json b/bower.json deleted file mode 100755 index 1dfea4f5..00000000 --- a/bower.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "coriolis_shipyard", - "authors": [ - "Colin McLeod " - ], - "description": "Coriolis Shipyard for Elite Dangerous", - "main": "app/app.js", - "keywords": [ - "elite", - "shipyard" - ], - "license": "MIT", - "private": true, - "ignore": [ - "**/.*", - "node_modules", - "bower_components", - "test", - "tests" - ], - "dependencies": { - "d3": "~3.5.5", - "ng-lodash": "~0.2.0", - "ui-router-extras": "0.0.13", - "angular-ui-router": "^0.2.15", - "d3-tip": "~0.6.7", - "ng-sortable": "~1.2.1", - "lz-string": "~1.4.4", - "angular": "~1.4.0", - "angular-translate": "~2.7.2" - }, - "overrides": { - "angular": { - "main": "angular.min.js" - }, - "angular-ui-router": { - "main": "release/angular-ui-router.min.js" - }, - "angular-translate": { - "main": "angular-translate.min.js" - }, - "d3": { - "main": "d3.min.js" - }, - "ng-lodash": { - "main": "build/ng-lodash.min.js" - }, - "ui-router-extras": { - "main": [ - "release/modular/ct-ui-router-extras.core.min.js", - "release/modular/ct-ui-router-extras.sticky.min.js" - ] - }, - "ng-sortable": { - "main": "dist/ng-sortable.min.js" - } - }, - "resolutions": { - "angular": "~1.4.0" - } -} diff --git a/data b/data deleted file mode 160000 index 0f2e563d..00000000 --- a/data +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0f2e563d0665d6397a5fa845db99b81e5ad9fc4c diff --git a/devServer.js b/devServer.js new file mode 100644 index 00000000..fc5a0098 --- /dev/null +++ b/devServer.js @@ -0,0 +1,21 @@ +var webpack = require('webpack'); +var WebpackDevServer = require("webpack-dev-server"); +var config = require('./webpack.config.dev'); + +new WebpackDevServer(webpack(config), { + publicPath: config.output.publicPath, + hot: true, + headers: { "Access-Control-Allow-Origin": "*" }, + 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); + } + + console.log("Listening at localhost:3300"); +}); diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100755 index 3a61d18a..00000000 --- a/gulpfile.js +++ /dev/null @@ -1,281 +0,0 @@ -// Build / Built-in dependencies -var gulp = require('gulp'), - exec = require('child_process').exec, - pkg = require('./package.json'); - -// Package.json / Gulp Dependencies -var appCache = require("gulp-manifest"), - concat = require('gulp-concat'), - del = require('del'), - eslint = require('gulp-eslint'); - gutil = require('gulp-util'), - htmlmin = require('gulp-htmlmin'), - jsonlint = require("gulp-jsonlint"), - karma = require('karma').server, - less = require('gulp-less'), - mainBowerFiles = require('main-bower-files'), - minifyCSS = require('gulp-minify-css'), - revAll = require('gulp-rev-all'), - runSequence = require('run-sequence'), - sourcemaps = require('gulp-sourcemaps'), - svgstore = require('gulp-svgstore'), - svgmin = require('gulp-svgmin'), - template = require('gulp-template'), - templateCache = require('gulp-angular-templatecache'), - uglify = require('gulp-uglify'); - -var cdnHostStr = ''; - -gulp.task('less', function() { - return gulp.src('app/less/app.less') - .pipe(less({paths: ['less/app.less']}).on('error',function(e){ - console.log('File:', e.fileName); - console.log('Line:', e.lineNumber); - console.log('Message:', e.message); - this.emit('end'); - })) - .pipe(minifyCSS()) - .pipe(gulp.dest('build')); -}); - -gulp.task('js-lint', function() { - return gulp.src(['app/js/**/*.js', '!app/js/template_cache.js', '!app/js/db.js']) - .pipe(eslint({ - globals: { angular:1, DB:1, d3:1, ga:1, GAPI_KEY:1, LZString: 1 }, - rules: { - quotes: [2, 'single'], - strict: 'global', - eqeqeq: 'smart', - 'space-after-keywords': [2, 'always'], - 'no-use-before-define': 'no-func', - 'space-before-function-paren': [2, 'never'], - 'space-before-blocks': [2, 'always'], - 'object-curly-spacing': [2, "always"], - 'brace-style': [2, '1tbs', { allowSingleLine: true }], - 'no-control-regex': false - }, - envs: ['browser'] - })) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - -gulp.task('json-lint', function() { - return gulp.src(['data/**/*.json' , 'app/schemas/**/*.json']) - .pipe(jsonlint()) - .pipe(jsonlint.reporter()) - .pipe(jsonlint.failAfterError()); -}); - -gulp.task('bower', function(){ - return gulp.src(mainBowerFiles()) - .pipe(uglify({mangle: false, compress: false}).on('error',function(e){ - console.log('Bower File:', e.fileName); - console.log('Line:', e.lineNumber); - console.log('Message:', e.message); - })) - .pipe(concat('lib.js')) - .pipe(gulp.dest('build')); -}); - -gulp.task('html2js', function() { - return gulp.src('app/views/**/*.html') - .pipe(htmlmin({ - 'collapseBooleanAttributes': true, - 'collapseWhitespace': true, - 'removeAttributeQuotes': true, - 'removeComments': true, - 'removeEmptyAttributes': true, - 'removeRedundantAttributes': true, - 'removeScriptTypeAttributes': true, - 'removeStyleLinkTypeAttributes': true - }).on('error',function(e){ - console.log('File:', e.fileName); - console.log('Message:',e.message); - })) - .pipe(templateCache({ - 'module': 'app.templates', - 'standalone': true, - 'root': 'views', - 'filename': 'template_cache.js' - })) - .pipe(gulp.dest('app/js')) -}); - -gulp.task('jsonToDB', function(cb) { - exec('node scripts/json-to-db.js', cb); -}); - -gulp.task('js', function() { - return gulp.src([ - 'app/js/db.js', - 'app/js/**/module-*.js', - 'app/js/template_cache.js', - 'app/js/app.js', - 'app/js/**/*.js' - ]) - .pipe(sourcemaps.init()) - .pipe(uglify({mangle: false}).on('error',function(e){ - console.log('File:', e.fileName); - console.log('Line:', e.lineNumber); - console.log('Message:', e.message); - this.emit('end'); - })) - .pipe(concat('app.js')) - .pipe(sourcemaps.write('.')) - .pipe(gulp.dest('build')); -}); - -gulp.task('copy', function() { - return gulp.src(['app/images/**','app/fonts/**','app/db.json', 'app/schemas/**'], {base: 'app/'}) - .pipe(gulp.dest('build')); -}); - -gulp.task('generateIndexHTML', function(done) { - // Generate minified inline svg of all icons for svg spriting - gulp.src('app/icons/*.svg') - .pipe(svgmin()) - .pipe(svgstore({ inlineSvg: true })) - .pipe(gutil.buffer(function(err, files) { - var svgIconsContent = files[0].contents.toString(); - gulp.src('app/index.html') - .pipe(template({ - version: pkg.version, - date : new Date().toISOString().slice(0, 10), - uaTracking: process.env.CORIOLIS_UA_TRACKING || false, - svgContent: svgIconsContent, - gapiKey: process.env.CORIOLIS_GAPI_KEY - })) - .pipe(htmlmin({ - 'collapseBooleanAttributes': true, - 'collapseWhitespace': true, - 'removeAttributeQuotes': true, - 'removeComments': true, - 'removeEmptyAttributes': true, - 'removeRedundantAttributes': true, - 'removeScriptTypeAttributes': true, - 'removeStyleLinkTypeAttributes': true - }).on('error',function(e){ - console.log('File:', e.fileName); - console.log('Message:',e.message); - })) - .pipe(gulp.dest('build')); - done(); - })); -}); - -gulp.task('serve', function(cb) { - exec('nginx -p $(pwd) -c nginx.conf', function (err, stdout, stderr) { - if (stderr) { - console.warn(stderr); - console.warn('Is NGINX already running?\n'); - } - cb(); - }); -}); - -// Windows command to launch nginx serv -gulp.task('serve-win', function(cb) { - exec('nginx -p %cd% -c nginx.conf', function (err, stdout, stderr) { - if (stderr) { - console.warn(stderr); - console.warn('Is NGINX already running?\n'); - } - cb(); - }); -}); - -gulp.task('serve-stop', function(cb) { - exec('kill -QUIT $(cat nginx.pid)', function (err, stdout, stderr) { - if (stderr) console.log(stderr); else cb(err); - }); -}); - -gulp.task('watch', function() { - gulp.watch(['app/index.html','app/icons/*.svg'], ['generateIndexHTML']); - gulp.watch(['app/images/**','app/fonts/**', 'app/db.json', 'app/schemas/**'], ['copy']); - gulp.watch('app/less/*.less', ['less']); - gulp.watch('app/views/**/*', ['html2js']); - gulp.watch('app/js/**/*.js', ['js']); - gulp.watch('data/**/*.json', ['jsonToDB']); - gulp.watch(['build/**', '!**/*.appcache'], ['appcache']); -}); - -gulp.task('cache-bust', function(done) { - var rev_all = new revAll({ prefix: cdnHostStr, dontRenameFile: ['.html','.json'] }); - var stream = gulp.src('build/**') - .pipe(rev_all.revision()) - .pipe(gulp.dest('build')) - .pipe(rev_all.manifestFile()) - .pipe(gulp.dest('build')); - - stream.on('end', function() { - var manifest = require('./build/rev-manifest.json'); - var arr = []; - for(var origFileName in manifest) { - if(origFileName != manifest[origFileName]) { // For all files busted/renamed - arr.push('./build/' + origFileName); // Add the original filename to the list - } - } - del(arr, done); // Delete all originals files the were not busted/renamed - }); - stream.on('error', done); -}); - -gulp.task('appcache', function(done) { - // Since using a CDN manually build file list rather than using appCache mechanisms - gulp.src(['build/**', '!build/index.html', '!**/*.json', '!**/logo/*', '!**/*.map','!**/*.appcache']) - .pipe(gutil.buffer(function(err, stream) { - var files = []; - for (var i = 0; i < stream.length; i++) { - if (!stream[i].isNull()) { - files.push(cdnHostStr + '/' + stream[i].relative); - } - } - - gulp.src([]) - .pipe(appCache({ - preferOnline: true, - cache: files, - filename: 'coriolis.appcache', - timestamp: true - })) - .pipe(gulp.dest('build')) - .on('end', done); - })); -}); - -gulp.task('upload', function(done) { - exec([ - "rsync -e 'ssh -i ", process.env.CORIOLIS_PEM, "' -a --delete build/ ", process.env.CORIOLIS_USER, "@", process.env.CORIOLIS_HOST, ":~/www" - ].join(''), - done - ); -}); - -gulp.task('test', function (done) { - karma.start({ - configFile: __dirname + '/test/karma.conf.js', - singleRun: true - }, function(exitStatus) { - done(exitStatus ? new gutil.PluginError('karma', { message: 'Unit tests failed!' }) : undefined); - }); -}); - -gulp.task('lint', ['js-lint', 'json-lint']); - -gulp.task('clean', function (done) { del(['build'], done); }); - -gulp.task('build', function (done) { runSequence('clean', ['html2js','jsonToDB'], ['generateIndexHTML','bower','js','less','copy'], done); }); -gulp.task('build-cache', function (done) { runSequence('build', 'appcache', done); }); -gulp.task('build-prod', function (done) { runSequence('build', 'cache-bust', 'appcache', done); }); - -gulp.task('dev', function (done) { runSequence('build-cache', 'serve','watch', done); }); - -gulp.task('deploy', function (done) { - cdnHostStr = '//cdn.' + process.env.CORIOLIS_HOST; - runSequence('build-prod', 'upload', done); -}); - -gulp.task('default', ['dev']); - diff --git a/nginx.conf b/nginx.conf old mode 100755 new mode 100644 index d9fa6a33..612029a2 --- a/nginx.conf +++ b/nginx.conf @@ -37,7 +37,7 @@ http { gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; server { - listen 3300; + listen 3301; server_name localhost; root ./build/; index index.html; @@ -56,3 +56,4 @@ http { } } } + diff --git a/package.json b/package.json index e989b427..417b1ea7 100644 --- a/package.json +++ b/package.json @@ -1,48 +1,95 @@ { "name": "coriolis_shipyard", - "version": "1.10.4", + "version": "2.0.0-Beta-1", "repository": { "type": "git", "url": "https://github.com/cmmcleod/coriolis" }, - "homepage": "http://coriolis.io", + "homepage": "https://coriolis.io", "bugs": "https://github.com/cmmcleod/coriolis/issues", "private": true, - "engine": "node >= 0.12.2", + "engine": "node >= 4.0.0", "license": "MIT", + "scripts": { + "extract-translations": "grep -hroE \"(translate\\('[^']+'\\))|(tip.bind\\(null, '[^']+')\" src/* | grep -oE \"'[^']+'\" | grep -oE \"[^']+\" | sort -u -f", + "clean": "rimraf build", + "start": "node devServer.js", + "lint": "eslint --ext .js,.jsx src", + "test": "jest", + "prod-serve": "nginx -p $(pwd) -c nginx.conf", + "prod-stop": "kill -QUIT $(cat nginx.pid)", + "build": "npm run clean && NODE_ENV=production webpack -d -p --config webpack.config.prod.js", + "rsync": "rsync -ae \"ssh -i $CORIOLIS_PEM\" --delete build/ $CORIOLIS_USER@$CORIOLIS_HOST:~/wwws", + "deploy": "npm run lint && npm test && npm run build:prod && npm run rsync" + }, + "jest": { + "scriptPreprocessor": "/node_modules/babel-jest", + "testFileExtensions": [ + "js" + ], + "moduleFileExtensions": [ + "js", + "json", + "jsx" + ], + "unmockedModulePathPatterns": [ + "/node_modules/react", + "/node_modules/react-dom", + "/node_modules/react-addons-test-utils", + "/node_modules/react-testutils-additions", + "/node_modules/fbjs", + "/node_modules/fbemitter", + "/node_modules/classnames", + "/node_modules/d3", + "/node_modules/lz-string", + "/node_modules/jsen", + "/node_modules/coriolis-data", + "/src/app/shipyard", + "/src/app/i18n", + "/src/app/utils", + "/src/schemas", + "/__tests__" + ] + }, "devDependencies": { - "angular-mocks": "1.4.x", - "async": "0.9.x", - "del": "1.2.x", - "gulp": "3.9.x", - "gulp-angular-templatecache": "1.6.x", - "gulp-concat": "2.5.x", - "gulp-eslint": "0.13.x", - "gulp-htmlmin": "1.1.x", - "gulp-jasmine": "2.0.x", - "gulp-jsonlint": "1.1.x", - "gulp-less": "3.0.x", - "gulp-manifest": "0.0.6", - "gulp-minify-css": "1.1.x", - "gulp-rev-all": "0.8.18", - "gulp-sourcemaps": "1.5.x", - "gulp-svgmin": "1.1.x", - "gulp-svgstore": "5.0.x", - "gulp-template": "3.0.x", - "gulp-uglify": "1.2.x", - "gulp-util": "3.0.x", - "jasmine-core": "2.3.x", + "appcache-webpack-plugin": "^1.2.1", + "babel-core": "*", + "babel-eslint": "*", + "babel-jest": "*", + "babel-loader": "*", + "babel-preset-es2015": "*", + "babel-preset-react": "*", + "babel-preset-stage-0": "*", + "css-loader": "^0.23.0", + "eslint": "2.2.0", + "eslint-plugin-react": "^4.0.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", + "jest-cli": "*", "jsen": "^0.6.0", - "json-concat": "0.0.x", - "karma": "0.12.x", - "karma-fixture": "^0.2.5", - "karma-jasmine": "0.3.x", - "karma-json-fixtures-preprocessor": "0.0.4", - "karma-mocha-reporter": "1.0.x", - "karma-phantomjs-launcher": "0.2.x", - "main-bower-files": "2.8.x", - "phantomjs": "1.9.x", - "run-sequence": "1.1.x", - "uglify-js": "2.4.x" + "json-loader": "^0.5.3", + "less": "^2.5.3", + "less-loader": "^2.2.1", + "react-addons-test-utils": "^0.14.7", + "react-testutils-additions": "^0.16.0", + "rimraf": "^2.4.3", + "style-loader": "^0.13.0", + "url-loader": "^0.5.6", + "webpack": "^1.9.6", + "webpack-dev-server": "^1.14.0" + }, + "dependencies": { + "babel-polyfill": "*", + "classnames": "^2.2.0", + "coriolis-data": "cmmcleod/coriolis-data", + "d3": "^3.5.9", + "fbemitter": "^2.0.0", + "lz-string": "^1.4.4", + "react": "^0.14.7", + "react-dom": "^0.14.7", + "superagent": "^1.4.0" } } diff --git a/scripts/json-to-db.js b/scripts/json-to-db.js deleted file mode 100755 index 67df0606..00000000 --- a/scripts/json-to-db.js +++ /dev/null @@ -1,90 +0,0 @@ -var fs = require('fs'); -var UglifyJS = require('uglify-js'); -var jsonConcat = require('json-concat'); -var async = require('async'); -var db_filename = './app/js/db.js'; - -async.parallel([ - function(cb) { jsonConcat({ dest: null, src: './data/ships' }, done.bind(cb)); }, - function(cb) { - var standard = [ - JSON.parse(fs.readFileSync('./data/components/standard/power_plant.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/thrusters.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/frame_shift_drive.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/life_support.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/power_distributor.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/sensors.json', 'utf8')), - JSON.parse(fs.readFileSync('./data/components/standard/fuel_tank.json', 'utf8')) - ]; - cb(null, standard); - }, - function(cb) { jsonConcat({ dest: null, src: './data/components/hardpoints' }, done.bind(cb)); }, - function(cb) { jsonConcat({ dest: null, src: './data/components/internal' }, done.bind(cb)); }, - function(cb) { jsonConcat({ dest: null, src: ['./data/components/bulkheads.json'] }, done.bind(cb)); } - ], writeDB); - -function done(err, json) { this(err,json); } - -function writeDB(err, arr) { - var ships = {}, internal = {}, hardpoints = {}; - var shipOrder = Object.keys(arr[0]).sort(function(a,b) { return arr[0][a].properties.name < arr[0][b].properties.name ? -1 : 1; }); - var internalOrder = Object.keys(arr[3]).sort(); - var hpOrder = [ - "pl", - "ul", - "bl", - "mc", - "c", - "fc", - "rg", - "pa", - "mr", - "tp", - "nl", - "ml", - "cs", - "cm", - "ws", - "kw", - "sb" - ]; - - for (var i = 0; i < internalOrder.length; i++) { - internal[internalOrder[i]] = arr[3][internalOrder[i]]; - } - - for (var j = 0; j < hpOrder.length; j++) { - hardpoints[hpOrder[j]] = arr[2][hpOrder[j]]; - } - - for (var s = 0; s < shipOrder.length; s++) { - ships[shipOrder[s]] = arr[0][shipOrder[s]]; - } - - try { - var db = { - ships: ships, - components: { - standard: arr[1], - hardpoints: hardpoints, - internal: internal, - bulkheads: arr[4] - } - }; - } - catch (e) { - console.error(arguments); - exit(0); - } - - var ast = UglifyJS.parse('var DB = ' + JSON.stringify(db)); - var code = ast.print_to_string({beautify: true, indent_level: 2}); - - fs.open(db_filename, 'w', function() { - fs.writeFile(db_filename, code, function(err) {}); - }); - - fs.open('./app/db.json', 'w', function() { - fs.writeFile('./app/db.json', JSON.stringify(db), function(err) {}); - }); -} diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx new file mode 100644 index 00000000..a362ea95 --- /dev/null +++ b/src/app/Coriolis.jsx @@ -0,0 +1,291 @@ +import React from 'react'; +import Router from './Router'; +import { EventEmitter } from 'fbemitter'; +import { getLanguage } from './i18n/Language'; +import Persist from './stores/Persist'; + +import Header from './components/Header'; +import Tooltip from './components/Tooltip'; +import ModalImport from './components/ModalImport'; + +import AboutPage from './pages/AboutPage'; +import NotFoundPage from './pages/NotFoundPage'; +import OutfittingPage from './pages/OutfittingPage'; +import ComparisonPage from './pages/ComparisonPage'; +import ShipyardPage from './pages/ShipyardPage'; +import ErrorDetails from './pages/ErrorDetails'; + +/** + * Coriolis App + */ +export default class Coriolis extends React.Component { + + static childContextTypes = { + language: React.PropTypes.object.isRequired, + sizeRatio: React.PropTypes.number.isRequired, + route: React.PropTypes.object.isRequired, + openMenu: React.PropTypes.func.isRequired, + closeMenu: React.PropTypes.func.isRequired, + showModal: React.PropTypes.func.isRequired, + hideModal: React.PropTypes.func.isRequired, + tooltip: React.PropTypes.func.isRequired, + termtip: React.PropTypes.func.isRequired, + onWindowResize: React.PropTypes.func.isRequired, + onCommand: React.PropTypes.func.isRequired + }; + + /** + * Creates an instance of the Coriolis App + */ + 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._tooltip = this._tooltip.bind(this); + this._termtip = this._termtip.bind(this); + this._onWindowResize = this._onWindowResize.bind(this); + this._onCommand = this._onCommand.bind(this); + this._onLanguageChange = this._onLanguageChange.bind(this); + this._onSizeRatioChange = this._onSizeRatioChange.bind(this); + this._keyDown = this._keyDown.bind(this); + + this.emitter = new EventEmitter(); + this.state = { + page: null, + language: getLanguage(Persist.getLangCode()), + route: null, + sizeRatio: Persist.getSizeRatio() + }; + + Router('', (r) => this._setPage(ShipyardPage, r)); + Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r)); + Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r)); + Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r)); + Router('/about', (r) => this._setPage(AboutPage, r)); + Router('*', (r) => this._setPage(null, r)); + } + + /** + * Updates / Sets the page and route context + * @param {[type]} page The page to be shown + * @param {Object} route The current route + */ + _setPage(page, route) { + this.setState({ page, route, currentMenu: null, modal: null, error: null }); + } + + /** + * Handle unexpected error. This is most likely an unhandled React Error which + * is also most likely unrecoverable. The best option is to catch as many details + * as possible so the user can report the error and provide a link to reload the page + * to reset the VM and clear any error state. + * + * @param {string} msg Message + * @param {string} scriptUrl URL + * @param {number} line Line number + * @param {number} col Column number + * @param {Object} errObj Error Object + */ + _onError(msg, scriptUrl, line, col, errObj) { + console && console.error && console.error(arguments); // eslint-disable-line no-console + this.setState({ + error: , + page: null, + currentMenu: null, + modal: null + }); + // TODO: Improve in the event of React Errors + // Potentially ReactDOM.render into dom here instead + // ReactDOM.render(this, document.getElementById('coriolis')); + } + + /** + * Propagate language and format changes + * @param {string} lang Language code + */ + _onLanguageChange(lang) { + this.setState({ language: getLanguage(Persist.getLangCode()) }); + } + + /** + * Propagate the sizeRatio change + * @param {number} sizeRatio Size ratio / scale + */ + _onSizeRatioChange(sizeRatio) { + this.setState({ sizeRatio }); + } + + /** + * Handle Key Down + * @param {Event} e Keyboard Event + */ + _keyDown(e) { + // .keyCode will eventually be replaced with .key + switch (e.keyCode) { + case 27: // Escape Key + this._hideModal(); + this._closeMenu(); + break; + case 73: // 'i' + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i + e.preventDefault(); + this._showModal(); + } + break; + case 101010: // 's' + if (e.ctrlKey || e.metaKey) { // CTRL/CMD + i + e.preventDefault(); + this.emitter.emit('command', 'save'); + } + } + } + + /** + * Opens the modal display with the specified content + * @param {React.Component} content Modal Content + */ + _showModal(content) { + let modal =
    this._hideModal() }>{content}
    ; + this.setState({ modal }); + } + + /** + * Hides any open modal + */ + _hideModal() { + if (this.state.modal) { + this.setState({ modal: null }); + } + } + + /** + * Sets the open menu state + * @param {string|object} currentMenu The reference to the current menu + */ + _openMenu(currentMenu) { + if (this.state.currentMenu != currentMenu) { + this.setState({ currentMenu }); + } + } + + /** + * Closes the open menu + */ + _closeMenu() { + if (this.state.currentMenu) { + this.setState({ currentMenu: null }); + } + } + + /** + * Show/Hide the tooltip + * @param {React.Component} content Tooltip content + * @param {DOMRect} rect Target bounding rect + * @param {[type]} opts Options + */ + _tooltip(content, rect, opts) { + if (!content && this.state.tooltip) { + this.setState({ tooltip: null }); + } else if (content && Persist.showTooltips()) { + this.setState({ tooltip: {content} }); + } + } + + /** + * Show the term tip + * @param {string} term Term or Phrase + * @param {Object} opts Options - dontCap, orientation (n,e,s,w) + * @param {SyntheticEvent} event Event + */ + _termtip(term, opts, event) { + if (opts && opts.nativeEvent) { // Opts is a SyntheticEvent + event = opts; + opts = { cap: true }; + } + this._tooltip( +
    {this.state.language.translate(term)}
    , + event.currentTarget.getBoundingClientRect(), + opts + ); + } + + /** + * Add a listener to on window resize + * @param {Function} listener Listener callback + * @return {Object} Subscription token + */ + _onWindowResize(listener) { + return this.emitter.addListener('windowResize', listener); + } + + /** + * Add a listener to global commands such as save, + * @param {Function} listener Listener callback + * @return {Object} Subscription token + */ + _onCommand(listener) { + return this.emitter.addListener('command', listener); + } + + /** + * Creates the context to be passed down to pages / components containing + * language, sizeRatio and route references + * @return {object} Context to be passed down + */ + getChildContext() { + return { + language: this.state.language, + route: this.state.route, + sizeRatio: this.state.sizeRatio, + openMenu: this._openMenu, + closeMenu: this._closeMenu, + showModal: this._showModal, + hideModal: this._hideModal, + tooltip: this._tooltip, + termtip: this._termtip, + onWindowResize: this._onWindowResize, + onCommand: this._onCommand + }; + } + + /** + * Adds necessary listeners and starts Routing + */ + componentWillMount() { + // Listen for appcache updated event, present refresh to update view + if (window.applicationCache) { + window.applicationCache.addEventListener('updateready', () => { + if (window.applicationCache.status == window.applicationCache.UPDATEREADY) { + this.setState({ appCacheUpdate: true }); // Browser downloaded a new app cache. + } + }); + } + + window.onerror = this._onError.bind(this); + window.addEventListener('resize', () => this.emitter.emit('windowResize')); + document.body.addEventListener('scroll', () => this._tooltip()); + document.addEventListener('keydown', this._keyDown); + Persist.addListener('language', this._onLanguageChange); + Persist.addListener('sizeRatio', this._onSizeRatioChange); + + Router.start(); + } + + /** + * Renders the main app + * @return {React.Component} The main app + */ + render() { + let currentMenu = this.state.currentMenu; + + return
    +
    + { this.state.error ? this.state.error : this.state.page ? React.createElement(this.state.page, { currentMenu }) : } + { this.state.modal } + { this.state.tooltip } +
    ; + } +} diff --git a/src/app/Router.js b/src/app/Router.js new file mode 100644 index 00000000..17a0dce5 --- /dev/null +++ b/src/app/Router.js @@ -0,0 +1,282 @@ +import Persist from './stores/Persist'; + +/** + * Determine if the app is running in mobile/tablet 'standalone' mode + * @return {Boolean} True if the app is in standalone mode + */ +function isStandAlone() { + try { + return window.navigator.standalone || (window.external && window.external.msIsSiteMode && window.external.msIsSiteMode()); + } catch (ex) { + return false; + } +} + +/** + * Register path with callback fn(), or route path`, or Router.start(). + * + * Router('*', fn); + * Router('/user/:id', load, user); + * Router('/user/' + user.id, { some: 'thing' }); + * Router('/user/' + user.id); + * Router(); + * + * @param {String} path path + * @param {Function} fn Callbacks (fn, fn, ...) + * @api public + */ +function Router(path, fn) { + let route = new Route(path); + for (let i = 1; i < arguments.length; ++i) { + Router.callbacks.push(route.middleware(arguments[i])); + } +} + +/** + * Callback functions. + */ + +Router.callbacks = []; + +Router.start = function() { + window.addEventListener('popstate', onpopstate, false); + + if (isStandAlone()) { + let state = Persist.getState(); + // If a previous state has been stored, load that state + if (state && state.name && state.params) { + Router(this.props.initialPath || '/'); + } else { + Router('/'); + } + } else { + let url = location.pathname + location.search; + Router.replace(url, null, true, true); + } +}; + +/** + * Show `path` with optional `state` object. + * + * @param {String} path Path + * @param {Object} state Additional state + * @return {Context} New Context + * @api public + */ +Router.go = function(path, state) { + gaTrack(path); + let ctx = new Context(path, state); + Router.dispatch(ctx); + if (!ctx.unhandled) { + history.pushState(ctx.state, ctx.title, ctx.canonicalPath); + } + return ctx; +}; + +/** + * Replace `path` with optional `state` object. + * + * @param {String} path path + * @param {Object} state State + * @param {Boolean} dispatch If true dispatch the route / trigger update + * @return {Context} New Context + * @api public + */ +Router.replace = function(path, state, dispatch) { + gaTrack(path); + let ctx = new Context(path, state); + if (dispatch) Router.dispatch(ctx); + history.replaceState(ctx.state, ctx.title, ctx.canonicalPath); + return ctx; +}; + +/** + * Dispatch the given `ctx`. + * + * @param {Context} ctx Context + * @api private + */ +Router.dispatch = function(ctx) { + let i = 0; + + /** + * Handle the next route + * @return {Function} Unhandled + */ + function next() { + let fn = Router.callbacks[i++]; + if (!fn) return unhandled(ctx); + fn(ctx, next); + } + + next(); +}; + +/** + * Unhandled `ctx`. When it's not the initial + * popstate then redirect. If you wish to handle + * 404s on your own use `Router('*', callback)`. + * + * @param {Context} ctx Context + * @return {Context} context + * @api private + */ +function unhandled(ctx) { + let current = window.location.pathname + window.location.search; + if (current != ctx.canonicalPath) { + window.location = ctx.canonicalPath; + } + return ctx; +} + +/** + * Initialize a new "request" `Context` + * with the given `path` and optional initial `state`. + * + * @param {String} path Path + * @param {Object} state State + * @api public + */ +function Context(path, state) { + let i = path.indexOf('?'); + + this.canonicalPath = path; + this.path = path || '/'; + this.title = document.title; + this.state = state || {}; + this.state.path = path; + this.querystring = ~i ? path.slice(i + 1) : ''; + this.pathname = ~i ? path.slice(0, i) : path; + this.params = {}; + + this.querystring.split('&').forEach((str) =>{ + let query = str.split('='); + this.params[query[0]] = decodeURIComponent(query[1]); + }, this); +} + +/** + * Initialize `Route` with the given HTTP `path`, + * and an array of `callbacks` and `options`. + * + * Options: + * + * - `sensitive` enable case-sensitive routes + * - `strict` enable strict matching for trailing slashes + * + * @param {String} path Path + * @param {Object} options Options + * @api private + */ +function Route(path, options) { + options = options || {}; + this.path = path; + this.method = 'GET'; + this.regexp = pathtoRegexp(path, this.keys = [], options.sensitive, options.strict); +} + +/** + * Return route middleware with + * the given callback `fn()`. + * + * @param {Function} fn Route function + * @return {Function} Callback + * @api public + */ +Route.prototype.middleware = function(fn) { + let self = this; + return function(ctx, next) { + if (self.match(ctx.path, ctx.params)) return fn(ctx, next); + next(); + }; +}; + +/** + * Check if this route matches `path`, if so + * populate `params`. + * + * @param {String} path Path + * @param {Array} params Path params + * @return {Boolean} True if path matches + * @api private + */ +Route.prototype.match = function(path, params) { + let keys = this.keys, + qsIndex = path.indexOf('?'), + pathname = ~qsIndex ? path.slice(0, qsIndex) : path, + m = this.regexp.exec(decodeURIComponent(pathname)); + + if (!m) return false; + + for (let i = 1, len = m.length; i < len; ++i) { + let key = keys[i - 1]; + + let val = 'string' == typeof m[i] ? decodeURIComponent(m[i]) : m[i]; + + if (key) { + params[key.name] = undefined !== params[key.name] ? params[key.name] : val; + } + } + + return true; +}; + +/** + * Track a page view in Google Analytics + * @param {string} path Path to track + */ +function gaTrack(path) { + if (window.ga) { + window.ga('send', 'pageview', { page: path }); + } +} + +/** + * Normalize the given path string, + * returning a regular expression. + * + * An empty array should be passed, + * which will contain the placeholder + * key names. For example "/user/:id" will + * then contain ["id"]. + * + * @param {String|RegExp|Array} path Path template(s) + * @param {Array} keys keys + * @param {Boolean} sensitive Case sensitive + * @param {Boolean} strict Strict matching + * @return {RegExp} Regular expression + * @api private + */ +function pathtoRegexp(path, keys, sensitive, strict) { + if (path instanceof RegExp) return path; + if (path instanceof Array) path = '(' + path.join('|') + ')'; + path = path + .concat(strict ? '' : '/?') + .replace(/\/\(/g, '(?:/') + .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, function(_, slash, format, key, capture, optional) { + keys.push({ name: key, optional: !! optional }); + slash = slash || ''; + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + + (optional || ''); + }) + .replace(/([\/.])/g, '\\$1') + .replace(/\*/g, '(.*)'); + return new RegExp('^' + path + '$', sensitive ? '' : 'i'); +} + +/** + * Handle "populate" events. + * @param {Event} e Event object + */ +function onpopstate(e) { + if (e.state) { + let path = e.state.path; + Router.replace(path, e.state, true); + } +} + +export default Router; diff --git a/src/app/components/ActiveLink.jsx b/src/app/components/ActiveLink.jsx new file mode 100644 index 00000000..5b6b2d15 --- /dev/null +++ b/src/app/components/ActiveLink.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import Link from './Link'; +import cn from 'classnames'; + + +/** + * Returns true if the current window location equals the link + * @param {string} href URL/Href + * @return {boolean} If matches + */ +function isActive(href) { + return href == (window.location.pathname + window.location.search); +} + +/** + * Active Link - Highlighted when URL matches window location + */ +export default class ActiveLink extends Link { + + /** + * Renders the component + * @return {React.Component} The active link + */ + render() { + let className = this.props.className; + if (isActive(this.props.href)) { + className = cn(className, 'active'); + } + + return {this.props.children}; + } + +} \ No newline at end of file diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx new file mode 100644 index 00000000..f99abf0d --- /dev/null +++ b/src/app/components/AvailableModulesMenu.jsx @@ -0,0 +1,238 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TranslatedComponent from './TranslatedComponent'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; +import cn from 'classnames'; +import { MountFixed, MountGimballed, MountTurret } from './SvgIcons'; + +const PRESS_THRESHOLD = 5000; // mouse/touch down threshold + +/** + * Available modules menu + */ +export default class AvailableModulesMenu extends TranslatedComponent { + + static propTypes = { + modules: React.PropTypes.oneOfType([React.PropTypes.object, React.PropTypes.array]).isRequired, + onSelect: React.PropTypes.func.isRequired, + diffDetails: React.PropTypes.func, + m: React.PropTypes.object, + shipMass: React.PropTypes.number, + warning: React.PropTypes.func + }; + + static defaultProps = { + shipMass: 0 + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + this._hideDiff = this._hideDiff.bind(this); + this.state = this._initState(props, context); + } + + /** + * Initiate the list of available moduels + * @param {Object} props React Component properties + * @param {Object} context React Component context + * @return {Object} list: Array of React Components, currentGroup Component if any + */ + _initState(props, context) { + let translate = context.language.translate; + let { m, warning, shipMass, onSelect, modules } = props; + let list, currentGroup; + let buildGroup = this._buildGroup.bind( + this, + translate, + m, + warning, + shipMass - (m && m.mass ? m.mass : 0), + (m, event) => { + this._hideDiff(event); + onSelect(m); + } + ); + + 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 + list.push(
    {translate('empty')}
    ); + for (let g in modules) { + if (m && g == m.grp) { + list.push(
    this.groupElem = elem} key={g} className={'select-group cap'}>{translate(g)}
    ); + } else { + list.push(
    {translate(g)}
    ); + } + + list.push(buildGroup(g, modules[g])); + } + } + + return { list, currentGroup }; + } + + /** + * Generate React Components for Module Group + * @param {Function} translate Translate function + * @param {Objecy} mountedModule Mounted Module + * @param {Funciton} warningFunc Warning function + * @param {number} mass Mass + * @param {function} onSelect Select/Mount callback + * @param {string} grp Group name + * @param {Array} modules Available modules + * @return {React.Component} Available Module Group contents + */ + _buildGroup(translate, mountedModule, warningFunc, mass, onSelect, grp, modules) { + let prevClass = null, prevRating = null; + let elems = []; + + for (let i = 0; i < modules.length; i++) { + let m = modules[i]; + let mount = null; + let disabled = m.maxmass && (mass + (m.mass ? m.mass : 0)) > m.maxmass; + let active = mountedModule && mountedModule === m; + let classes = cn(m.name ? 'lc' : 'c', { + warning: !disabled && warningFunc && warningFunc(m), + active, + disabled + }); + let eventHandlers; + + if (disabled || active) { + eventHandlers = {}; + } else { + 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 + }; + } + + 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.mount) && m.grp != 'pa') { + elems.push(
    ); + } + + elems.push( +
  • + {mount} + {(mount ? ' ' : '') + m.class + m.rating + (m.missile ? '/' + m.missile : '') + (m.name ? ' ' + translate(m.name) : '')} +
  • + ); + prevClass = m.class; + prevRating = m.rating; + } + + return
      {elems}
    ; + } + + /** + * 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 {DOMRect} rect DOMRect for target element + */ + _showDiff(mm, m, rect) { + if (this.props.diffDetails) { + this.touchTimeout = null; + this.context.tooltip(this.props.diffDetails(m, mm), rect); + } + } + + /** + * Mouse over diff handler + * @param {Function} showDiff diff tooltip callback + * @param {SyntheticEvent} event Event + */ + _over(showDiff, event) { + event.preventDefault(); + showDiff(event.currentTarget.getBoundingClientRect()); + } + + /** + * Toucch Start - Show diff after press, otherwise treat as tap + * @param {Function} showDiff diff tooltip callback + * @param {SyntheticEvent} event Event + */ + _touchStart(showDiff, event) { + let rect = event.currentTarget.getBoundingClientRect(); + this.touchTimeout = setTimeout(showDiff.bind(this, rect), PRESS_THRESHOLD); + } + + /** + * Touch End - Select module on tap + * @param {Function} select Select module callback + * @param {SyntheticEvent} event Event + */ + _touchEnd(select, event) { + if (this.touchTimeout !== null) { // If timeout has not fired (been nulled out) yet + select(); + } + event.preventDefault(); + this._hideDiff(); + } + + /** + * Hide diff tooltip + * @param {SyntheticEvent} event Event + */ + _hideDiff(event) { + clearTimeout(this.touchTimeout); + this.touchTimeout = null; + this.context.tooltip(); + } + + /** + * Scroll to mounted (if it exists) module group on mount + */ + componentDidMount() { + if (this.groupElem) { // Scroll to currently selected group + findDOMNode(this).scrollTop = this.groupElem.offsetTop; + } + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + this.setState(this._initState(nextProps, nextContext)); + } + + /** + * Render the list + * @return {React.Component} List + */ + render() { + return ( +
    e.stopPropagation() } + onContextMenu={stopCtxPropagation} + > + {this.state.list} +
    + ); + } + +} diff --git a/src/app/components/BarChart.jsx b/src/app/components/BarChart.jsx new file mode 100644 index 00000000..a53c3d48 --- /dev/null +++ b/src/app/components/BarChart.jsx @@ -0,0 +1,218 @@ +import React from 'react'; +import d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; + +const MARGIN = { top: 15, right: 20, bottom: 40, left: 150 }; +const BAR_HEIGHT = 30; + +/** + * Get ship and build name + * @param {Object} build Ship build + * @return {string} name and build name + */ +function bName(build) { + return build.buildName + '\n' + build.name; +} + +/** + * Replace a SVG text element's content with + * tspans that wrap on newline + * @param {string} d Data point + */ +function insertLinebreaks(d) { + let el = d3.select(this); + let lines = d.split('\n'); + el.text('').attr('y', -6); + for (let i = 0; i < lines.length; i++) { + let tspan = el.append('tspan').text(lines[i].length > 18 ? lines[i].substring(0, 15) + '...' : lines[i]); + if (i > 0) { + tspan.attr('x', -9).attr('dy', '1em'); + } else { + tspan.attr('class', 'primary'); + } + } +} + +/** + * Bar Chart + */ +export default class BarChart extends TranslatedComponent { + + static defaultProps = { + colors: ['#7b6888', '#6b486b', '#3182bd', '#a05d56', '#d0743c'], + labels: null, + unit: '' + }; + + static PropTypes = { + colors: React.PropTypes.array, + data: React.PropTypes.array.isRequired, + desc: React.PropTypes.bool, + format: React.PropTypes.string.isRequired, + labels: React.PropTypes.array, + predicate: React.PropTypes.string, + properties: React.PropTypes.array, + title: React.PropTypes.string.isRequired, + unit: React.PropTypes.string.isRequired, + width: React.PropTypes.number.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._updateDimensions = this._updateDimensions.bind(this); + this._hideTip = this._hideTip.bind(this); + + let scale = d3.scale.linear(); + let y0 = d3.scale.ordinal(); + let y1 = d3.scale.ordinal(); + + this.xAxis = d3.svg.axis().scale(scale).ticks(5).outerTickSize(0).orient('bottom').tickFormat(context.language.formats.s2); + this.yAxis = d3.svg.axis().scale(y0).outerTickSize(0).orient('left'); + this.state = { scale, y0, y1, color: d3.scale.ordinal().range(props.colors) }; + } + + /** + * Generate and Show tooltip + * @param {Object} build Ship build + * @param {string} property Property to display + * @param {number} propertyIndex Property Label index + */ + _showTip(build, property, propertyIndex) { + let { unit, format, labels } = this.props; + let { scale, y0, y1 } = this.state; + let { translate, formats } = this.context.language; + let fontSize = parseFloat(window.getComputedStyle(document.getElementById('coriolis')).getPropertyValue('font-size') || 16); + let val = build[property]; + let lblStr = labels ? translate(labels[propertyIndex]) + ': ' : ''; + let valStr = formats[format](val) + ' ' + unit; + let valMidPoint = scale(val) / 2; + let y = y0(bName(build)) + y1(property) - fontSize - 5; + let width = ((lblStr.length + valStr.length) / 1.8) * fontSize; + let midPoint = width / 2; + + let tooltip = + + + + {lblStr} + {valStr} + + + + ; + this.setState({ tooltip }); + } + + /** + * Hide tooltip + */ + _hideTip() { + this.setState({ tooltip: null }); + } + + /** + * Update dimensions based on properties and scale + * @param {Object} props React Component properties + * @param {number} scale size ratio / scale + */ + _updateDimensions(props, scale) { + let { width, data, properties } = props; + let innerWidth = width - MARGIN.left - MARGIN.right; + let barHeight = Math.round(BAR_HEIGHT * scale); + let dataSize = data.length; + let innerHeight = barHeight * dataSize; + let outerHeight = innerHeight + MARGIN.top + MARGIN.bottom; + let max = data.reduce((max, build) => (properties.reduce(((m, p) => (m > build[p] ? m : build[p])), max)), 0); + + this.state.scale.range([0, innerWidth]).domain([0, max]); + this.state.y0.domain(data.map(bName)).rangeRoundBands([0, innerHeight], 0.3); + this.state.y1.domain(properties).rangeRoundBands([0, this.state.y0.rangeBand()]); + + this.setState({ + barHeight, + dataSize, + innerWidth, + outerHeight, + innerHeight + }); + } + + /** + * Update dimensions based on props and context. + */ + componentWillMount() { + this._updateDimensions(this.props, this.context.sizeRatio); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + let { data, width, predicate, desc } = nextProps; + let props = this.props; + + if (width != props.width || this.context.sizeRatio != nextContext.sizeRatio || data != props.data) { + this._updateDimensions(nextProps, nextContext.sizeRatio); + } + + if (this.context.language != nextContext.language) { + this.xAxis.tickFormat(nextContext.language.formats.s2); + } + + if (predicate != props.predicate || desc != props.desc) { + this.state.y0.domain(data.map(bName)); + } + } + + /** + * Render the chart + * @return {React.Component} Chart SVG + */ + render() { + if (!this.props.width) { + return null; + } + + let { title, unit, width, data, properties } = this.props; + let { innerWidth, outerHeight, innerHeight, y0, y1, scale, color, tooltip } = this.state; + + let bars = data.map((build, i) => + + { properties.map((p, propIndex) => + + )} + + ); + + return + + {bars} + {tooltip} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {title} + { unit ? {` (${unit})`} : null } + + + { let e = d3.select(elem); e.call(this.yAxis); e.selectAll('text').each(insertLinebreaks); }} /> + + ; + } +} diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx new file mode 100644 index 00000000..ca99d307 --- /dev/null +++ b/src/app/components/ComparisonTable.jsx @@ -0,0 +1,132 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Link from './Link'; +import cn from 'classnames'; +import { SizeMap } from '../shipyard/Constants'; + + +/** + * Comparison Table + */ +export default class ComparisonTable extends TranslatedComponent { + + static propTypes = { + facets: React.PropTypes.array.isRequired, + builds: React.PropTypes.array.isRequired, + onSort: React.PropTypes.func.isRequired, + predicate: React.PropTypes.string.isRequired, // Used only to test again prop changes for shouldRender + desc: React.PropTypes.oneOfType([React.PropTypes.bool.isRequired, React.PropTypes.number.isRequired]), // Used only to test again prop changes for shouldRender + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props, context); + this._buildHeaders = this._buildHeaders.bind(this); + this.state = this._buildHeaders(props.facets, props.onSort, context.language.translate); + } + + /** + * Build table headers + * @param {Array} facets Facets list + * @param {Function} onSort Sort callback + * @param {Function} translate Translate function + * @return {Object} Header Components + */ + _buildHeaders(facets, onSort, translate) { + let header = [ + {translate('ship')}, + {translate('build')} + ]; + let subHeader = []; + + for (let f of facets) { + if (f.active) { + let p = f.props; + let pl = p.length; + header.push( + {translate(f.title)} + ); + + if (pl > 1) { + for (let i = 0; i < pl; i++) { + subHeader.push({translate(f.lbls[i])}); + } + } + } + } + + return { header, subHeader }; + } + + /** + * Generate a table row for the build + * @param {Object} build Ship build + * @param {Array} facets Facets list + * @param {Object} formats Localized formats map + * @param {Object} units Localized untis map + * @return {React.Component} Table row + */ + _buildRow(build, facets, formats, units) { + let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`; + let cells = [ + {build.name}, + {build.buildName} + ]; + + for (let f of facets) { + if (f.active) { + for (let p of f.props) { + cells.push({formats[f.fmt](build[p])}{f.unit ? units[f.unit] : null}); + } + } + } + + return {cells}; + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + // If facets or language has changed re-render header + if (nextProps.facets != this.props.facets || nextContext.language != this.context.language) { + this.setState(this._buildHeaders(nextProps.facets, nextProps.onSort, nextContext.language.translate)); + } + } + + /** + * Render the table + * @return {React.Component} Comparison table + */ + render() { + let { builds, facets } = this.props; + let { header, subHeader } = this.state; + let { formats, units } = this.context.language; + + let buildsRows = new Array(builds.length); + + for (let i = 0, l = buildsRows.length; i < l; i++) { + buildsRows[i] = this._buildRow(builds[i], facets, formats, units); + } + + return ( +
    + + + {header} + {subHeader} + + + {buildsRows} + +
    +
    + ); + } +} diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx new file mode 100644 index 00000000..d0d172d6 --- /dev/null +++ b/src/app/components/CostSection.jsx @@ -0,0 +1,680 @@ +import React from 'react'; +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'; + +/** + * Cost Section + */ +export default class CostSection extends TranslatedComponent { + + static PropTypes = { + ship: React.PropTypes.object.isRequired, + code: React.PropTypes.string.isRequired, + buildName: React.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); + + 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)}]`}} + {moduleDiscount && {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}} + {translate('credits')}
    {translate('total')}{formats.int(ship.totalCost)}{units.CR}
    {translate('insurance')}{formats.int(ship.totalCost * insurance)}{units.CR}
    +
    ; + } + + /** + * Render the retofit tab + * @return {React.Component} Tab contents + */ + _retrofitTab() { + let { retrofitTotal, retrofitCosts, moduleDiscount, retrofitName } = this.state; + 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 < 1 && {`[${translate('modules')} -${formats.pct(moduleDiscount)}]`}} +
    {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.index != retrofitShip.bulkheads.index) { + 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 + }; + 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++) { + if (slotGroup[i].m != retroSlotGroup[i].m) { + item = { netCost: 0, retroItem: retroSlotGroup[i] }; + if (slotGroup[i].m) { + 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.cells; + break; + case 'am': + q = slotGroup[i].m.ammo; + break; + case 'pv': + srvs += slotGroup[i].m.vehicles; + 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; + } + + if (srvs > 0) { + item = { + m: { name: 'SRVs', class: '', rating: '' }, + max: srvs, + cost: 6005, + total: srvs * 6005 + }; + 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) { + 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} +
    + ); + } +} diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx new file mode 100644 index 00000000..1796b739 --- /dev/null +++ b/src/app/components/HardpointSlot.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import Slot from './Slot'; + +/** + * 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 {Function} translate Translate function + * @param {Object} formats Localized Formats map + * @param {Object} u Localized Units Map + * @return {React.Component} Slot contents + */ + _getSlotDetails(m, translate, formats, u) { + if (m) { + let classRating = `${m.class}${m.rating}${m.mount ? '/' + m.mount : ''}${m.missile ? m.missile : ''}`; + let { drag, drop } = this.props; + + return
    +
    +
    {classRating + ' ' + translate(m.name || m.grp)}
    +
    {m.mass}{u.T}
    +
    +
    + { m.damage ?
    {translate('damage')}: {m.damage} { m.ssdam ? ({formats.int(m.ssdam)} {u.MJ}) : null }
    : null } + { m.dps ?
    {translate('DPS')}: {m.dps} { m.mjdps ? ({formats.int(m.mjdps)} {u.MJ}) : null }
    : null } + { m.thermload ?
    {translate('T-Load')}: {m.thermload}
    : null } + { m.type ?
    {translate('type')}: {m.type}
    : null } + { m.rof ?
    {translate('ROF')}: {m.rof}{u.ps}
    : null } + { m.armourpen ?
    {translate('pen')}: {m.armourpen}
    : null } + { m.shieldmul ?
    +{formats.rPct(m.shieldmul)}
    : null } + { m.range ?
    {m.range} km
    : null } + { m.ammo >= 0 ?
    {translate('ammo')}: {formats.int(m.clip)}+{formats.int(m.ammo)}
    : null } +
    +
    ; + } else { + return
    {translate('empty')}
    ; + } + } +} diff --git a/src/app/components/HardpointsSlotSection.jsx b/src/app/components/HardpointsSlotSection.jsx new file mode 100644 index 00000000..7591d099 --- /dev/null +++ b/src/app/components/HardpointsSlotSection.jsx @@ -0,0 +1,131 @@ +import React from 'react'; +import SlotSection from './SlotSection'; +import HardpointSlot from './HardpointSlot'; +import cn from 'classnames'; +import { MountFixed, MountGimballed, MountTurret } from '../components/SvgIcons'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; + +/** + * Hardpoint slot section + */ +export default class HardpointsSlotSection 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); + } + + /** + * Empty all slots + */ + _empty() { + this.props.ship.emptyWeapons(); + this.props.onChange(); + this._close(); + } + + /** + * Fill slots with specified module + * @param {string} group Group name + * @param {string} mount Mount Type - F, G, T + * @param {SyntheticEvent} event Event + */ + _fill(group, mount, event) { + this.props.ship.useWeapon(group, mount, null, event.getModifierState('Alt')); + this.props.onChange(); + this._close(); + } + + /** + * Empty all on section header right click + */ + _contextMenu() { + this._empty(); + } + + /** + * Generate the slot React Components + * @return {Array} Array of Slots + */ + _getSlots() { + let { ship, currentMenu } = 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( availableModules.getHps(h.maxClass)} + onOpen={this._openMenu.bind(this, h)} + onSelect={this._selectModule.bind(this, h)} + 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} + />); + } + } + + return slots; + } + + /** + * Generate the section drop-down menu + * @param {Function} translate Translate function + * @return {React.Component} Section menu + */ + _getSectionMenu(translate) { + let _fill = this._fill; + + return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}> +
      +
    • {translate('empty all')}
    • +
    +
    {translate('pl')}
    +
      +
    • +
    • +
    • +
    +
    {translate('ul')}
    +
      +
    • +
    • +
    • +
    +
    {translate('bl')}
    +
      +
    • +
    • +
    • +
    +
    {translate('mc')}
    +
      +
    • +
    • +
    • +
    +
    {translate('c')}
    +
      +
    • +
    • +
    • +
    +
    ; + } + +} diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx new file mode 100644 index 00000000..cd214c73 --- /dev/null +++ b/src/app/components/Header.jsx @@ -0,0 +1,505 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Languages } from '../i18n/Language'; +import { Insurance } from '../shipyard/Constants'; +import Link from './Link'; +import ActiveLink from './ActiveLink'; +import cn from 'classnames'; +import { Cogs, CoriolisLogo, Hammer, Rocket, StatsBars } from './SvgIcons'; +import { Ships } from 'coriolis-data/dist'; +import Persist from '../stores/Persist'; +import { toDetailedExport } from '../shipyard/Serializer'; +import ModalDeleteAll from './ModalDeleteAll'; +import ModalExport from './ModalExport'; +import ModalImport from './ModalImport'; +import Slider from './Slider'; +import { outfitURL } from '../utils/UrlGenerators'; + +const SIZE_MIN = 0.65; +const SIZE_RANGE = 0.55; + +/** + * Normalize percentages to 'clean' values + * @param {Number} val Percentage value + * @return {Number} Normalized value + */ +function normalizePercent(val) { + if (val === '' || isNaN(val)) { + return 0; + } + val = Math.round(val * 100) / 100; + return val >= 100 ? 100 : val; +} + +/** + * Rounds the value to the nearest quarter (0, 0.25, 0.5, 0.75) + * @param {Number} val Value + * @return {Number} Rounded value + */ +function nearestQtrPct(val) { + return Math.round(val * 4) / 4; +} + +/** + * Select all text in a field + * @param {SyntheticEvent} e Event + */ +function selectAll(e) { + e.target.select(); +} + +/** + * 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(props, context) { + super(props); + this.shipOrder = Object.keys(Ships).sort(); + + this._setLanguage = this._setLanguage.bind(this); + this._setInsurance = this._setInsurance.bind(this); + this._setShipDiscount = this._setShipDiscount.bind(this); + this._changeShipDiscount = this._changeShipDiscount.bind(this); + this._kpShipDiscount = this._kpShipDiscount.bind(this); + this._setModuleDiscount = this._setModuleDiscount.bind(this); + this._changeModuleDiscount = this._changeModuleDiscount.bind(this); + this._kpModuleDiscount = this._kpModuleDiscount.bind(this); + this._openShips = this._openMenu.bind(this, 's'); + this._openBuilds = this._openMenu.bind(this, 'b'); + this._openComp = this._openMenu.bind(this, 'comp'); + this._openSettings = this._openMenu.bind(this, 'settings'); + this.languageOptions = []; + this.insuranceOptions = []; + this.state = { + shipDiscount: normalizePercent(Persist.getShipDiscount() * 100), + moduleDiscount: normalizePercent(Persist.getModuleDiscount() * 100), + }; + + let translate = context.language.translate; + + for (let langCode in Languages) { + this.languageOptions.push(); + } + + for (let name in Insurance) { + this.insuranceOptions.push(); + } + } + + /** + * Update insurance level + * @param {SyntheticEvent} e Event + */ + _setInsurance(e) { + Persist.setInsurance(e.target.value); + } + + /** + * Update the Module discount + */ + _setModuleDiscount() { + let moduleDiscount = normalizePercent(this.state.moduleDiscount); + this.setState({ moduleDiscount }); + Persist.setModuleDiscount(moduleDiscount / 100); // Decimal value is stored + } + + /** + * Update the Ship discount + */ + _setShipDiscount() { + let shipDiscount = normalizePercent(this.state.shipDiscount); + this.setState({ shipDiscount }); + Persist.setShipDiscount(shipDiscount / 100); // Decimal value is stored + } + + /** + * Input handler for the module discount field + * @param {SyntheticEvent} e Event + */ + _changeModuleDiscount(e) { + let moduleDiscount = e.target.value; + + if (e.target.value === '' || e.target.value === '-' || e.target.value === '.') { + this.setState({ moduleDiscount }); + } else if (!isNaN(moduleDiscount) && Math.round(moduleDiscount) < 100) { + this.setState({ moduleDiscount }); + } + } + + /** + * Input handler for the ship discount field + * @param {SyntheticEvent} e Event + */ + _changeShipDiscount(e) { + let shipDiscount = e.target.value; + + if (e.target.value === '' || e.target.value === '-' || e.target.value === '.') { + this.setState({ shipDiscount }); + } else if (!isNaN(shipDiscount) && Math.round(shipDiscount) < 100) { + this.setState({ shipDiscount }); + } + } + + /** + * Key down/press handler for ship discount field + * @param {SyntheticEvent} e Event + */ + _kpShipDiscount(e) { + let sd = this.state.shipDiscount * 1; + switch (e.keyCode) { + case 38: + e.preventDefault(); + this.setState({ shipDiscount: e.shiftKey ? nearestQtrPct(sd + 0.25) : normalizePercent(sd + 1) }); + break; + case 40: + e.preventDefault(); + this.setState({ shipDiscount: e.shiftKey ? nearestQtrPct(sd - 0.25) : normalizePercent(sd - 1) }); + break; + case 13: + e.preventDefault(); + e.target.blur(); + } + } + + /** + * Key down/press handler for module discount field + * @param {SyntheticEvent} e Event + */ + _kpModuleDiscount(e) { + let md = this.state.moduleDiscount * 1; + switch (e.keyCode) { + case 38: + e.preventDefault(); + this.setState({ moduleDiscount: e.shiftKey ? nearestQtrPct(md + 0.25) : normalizePercent(md + 1) }); + break; + case 40: + e.preventDefault(); + this.setState({ moduleDiscount: e.shiftKey ? nearestQtrPct(md - 0.25) : normalizePercent(md - 1) }); + break; + case 13: + e.preventDefault(); + e.target.blur(); + } + } + + /** + * Update the current language + * @param {SyntheticEvent} e Event + */ + _setLanguage(e) { + Persist.setLangCode(e.target.value); + } + + /** + * Toggle tooltips setting + */ + _toggleTooltips() { + Persist.showTooltips(!Persist.showTooltips()); + } + + /** + * Show delete all modal + * @param {SyntheticEvent} e Event + */ + _showDeleteAll(e) { + e.preventDefault(); + this.context.showModal(); + }; + + /** + * Show export modal with backup data + * @param {SyntheticEvent} e Event + */ + _showBackup(e) { + let translate = this.context.language.translate; + e.preventDefault(); + this.context.showModal(); + }; + + /** + * Show export modal with detailed export + * @param {SyntheticEvent} e Event + */ + _showDetailedExport(e) { + let translate = this.context.language.translate; + e.preventDefault(); + + this.context.showModal(); + } + + /** + * Show import modal + * @param {SyntheticEvent} e Event + */ + _showImport(e) { + e.preventDefault(); + this.context.showModal(); + } + + /** + * Update the app scale / size ratio + * @param {number} scale scale Size Ratio + */ + _setTextSize(scale) { + Persist.setSizeRatio((scale * SIZE_RANGE) + SIZE_MIN); + } + + /** + * Reset the app scale / size ratio + */ + _resetTextSize() { + Persist.setSizeRatio(1); + } + + /** + * Open a menu + * @param {string} menu Menu name + * @param {SyntheticEvent} event Event + */ + _openMenu(menu, event) { + event.stopPropagation(); + if (this.props.currentMenu == menu) { + menu = null; + } + + this.context.openMenu(menu); + } + + /** + * Generate the ships menu + * @return {React.Component} Menu + */ + _getShipsMenu() { + let shipList = []; + + for (let s in Ships) { + shipList.push({Ships[s].properties.name}); + } + + return ( +
    e.stopPropagation() }> + {shipList} +
    + ); + } + + /** + * Generate the builds menu + * @return {React.Component} Menu + */ + _getBuildsMenu() { + let builds = Persist.getBuilds(); + let buildList = []; + for (let shipId of this.shipOrder) { + if (builds[shipId]) { + let shipBuilds = []; + let buildNameOrder = Object.keys(builds[shipId]).sort(); + for (let buildName of buildNameOrder) { + let href = outfitURL(shipId, builds[shipId][buildName], buildName); + shipBuilds.push(
  • {buildName}
  • ); + } + buildList.push(
      {Ships[shipId].properties.name}{shipBuilds}
    ); + } + } + + return ( +
    e.stopPropagation() }> +
    {buildList}
    +
    + ); + } + + /** + * Generate the comparison menu + * @return {React.Component} Menu + */ + _getComparisonsMenu() { + let comparisons; + let translate = this.context.language.translate; + + if (Persist.hasComparisons()) { + comparisons = []; + let comps = Object.keys(Persist.getComparisons()).sort(); + + for (let name of comps) { + comparisons.push({name}); + } + } else { + comparisons = {translate('none created')}; + } + + return ( +
    e.stopPropagation() } style={{ whiteSpace: 'nowrap' }}> + {comparisons} +
    + {translate('compare all')} + {translate('create new')} +
    + ); + } + + /** + * Generate the settings menu + * @return {React.Component} Menu + */ + _getSettingsMenu() { + let translate = this.context.language.translate; + let tips = Persist.showTooltips(); + + return ( +
    e.stopPropagation() }> +
    + {translate('language')} + +
    + + {translate('tooltips')} +
    {(tips ? '✓' : '✗')}
    +
    +
    + {translate('insurance')} + +
    + {translate('ship')} {translate('discount')} + + % +
    + {translate('module')} {translate('discount')} + + % +
    +
    +
      + {translate('builds')} & {translate('comparisons')} +
    • {translate('backup')}
    • +
    • {translate('detailed export')}
    • +
    • {translate('import')}
    • +
    • {translate('delete all')}
    • +
    +
    + + + + + + + + + + + +
    AA
    {translate('reset')}
    +
    + {translate('about')} +
    + ); + } + + /** + * Add listeners on mount + */ + componentWillMount() { + let update = () => this.forceUpdate(); + Persist.addListener('language', update); + Persist.addListener('insurance', update); + // Persist.addListener('discounts', update); + Persist.addListener('deletedAll', update); + Persist.addListener('builds', update); + Persist.addListener('tooltips', update); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + if(this.context.language != nextContext.language) { + let translate = nextContext.language.translate; + this.insuranceOptions = []; + for (let name in Insurance) { + this.insuranceOptions.push(); + } + } + if (nextProps.currentMenu == 'settings') { // Settings menu is about to be opened + this.setState({ + shipDiscount: normalizePercent(Persist.getShipDiscount() * 100), + moduleDiscount: normalizePercent(Persist.getModuleDiscount() * 100), + }); + } else if (this.props.currentMenu == 'settings') { // Settings menu is about to be closed + if (this.state.shipDiscount != (Persist.getShipDiscount() * 100)) { + this._setShipDiscount(); + } + if (this.state.moduleDiscount != (Persist.getModuleDiscount() * 100)) { + this._setModuleDiscount(); + } + } + } + + /** + * Render the header + * @return {React.Component} Header + */ + render() { + let translate = this.context.language.translate; + let openedMenu = this.props.currentMenu; + let hasBuilds = Persist.hasBuilds(); + + if (this.props.appCacheUpdate) { + return
    window.location.reload() }>{translate('PHRASE_UPDATE_RDY')}
    ; + } + + return ( +
    + + +
    +
    + {' ' + translate('ships')} +
    + {openedMenu == 's' ? this._getShipsMenu() : null} +
    + +
    +
    + {' ' + translate('builds')} +
    + {openedMenu == 'b' ? this._getBuildsMenu() : null} +
    + +
    +
    + {' ' + translate('compare')} +
    + {openedMenu == 'comp' ? this._getComparisonsMenu() : null} +
    + +
    +
    + {translate('settings')} +
    + {openedMenu == 'settings' ? this._getSettingsMenu() : null} +
    +
    + ); + } + +} \ No newline at end of file diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx new file mode 100644 index 00000000..fcfb322d --- /dev/null +++ b/src/app/components/InternalSlot.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import Slot from './Slot'; +import { Infinite } from './SvgIcons'; + +/** + * Internal Slot + */ +export default class InternalSlot extends Slot { + + /** + * Generate the slot contents + * @param {Object} m Mounted Module + * @param {Function} translate Translate function + * @param {Object} formats Localized Formats map + * @param {Object} u Localized Units Map + * @return {React.Component} Slot contents + */ + _getSlotDetails(m, translate, formats, u) { + if (m) { + let classRating = m.class + m.rating; + let { drag, drop } = this.props; + + return
    +
    +
    {classRating + ' ' + translate(m.name || m.grp)}
    +
    {m.mass || m.cargo || m.fuel || 0}{u.T}
    +
    +
    + { m.optmass ?
    {translate('optimal mass') + ': '}{m.optmass}{u.T}
    : null } + { m.maxmass ?
    {translate('max mass') + ': '}{m.maxmass}{u.T}
    : null } + { m.bins ?
    {m.bins + ' '}{translate('bins')}
    : null } + { m.bays ?
    {translate('bays') + ': ' + m.bays}
    : null } + { m.rate ?
    {translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
    : null } + { m.ammo ?
    {translate('ammo')}: {formats.gen(m.ammo)}
    : null } + { m.cells ?
    {translate('cells')}: {m.cells}
    : null } + { m.recharge ?
    {translate('recharge')}: {m.recharge} MJ   {translate('total')}: {m.cells * m.recharge}{u.MJ}
    : null } + { m.repair ?
    {translate('repair')}: {m.repair}
    : null } + { m.range ?
    {translate('range')} {m.range}{u.km}
    : null } + { m.time ?
    {translate('time')}: {formats.time(m.time)}
    : null } + { m.maximum ?
    {translate('max')}: {(m.maximum)}
    : null } + { m.rangeLS ?
    {m.rangeLS}{u.Ls}
    : null } + { m.rangeLS === null ?
    {u.Ls}
    : null } + { m.rangeRating ?
    {translate('range')}: {m.rangeRating}
    : null } + { m.armouradd ?
    +{m.armouradd} {translate('armour')}
    : null } +
    +
    ; + } else { + return
    {translate('empty')}
    ; + } + } +} diff --git a/src/app/components/InternalSlotSection.jsx b/src/app/components/InternalSlotSection.jsx new file mode 100644 index 00000000..ca9d197c --- /dev/null +++ b/src/app/components/InternalSlotSection.jsx @@ -0,0 +1,145 @@ +import React from 'react'; +import cn from 'classnames'; +import SlotSection from './SlotSection'; +import InternalSlot from './InternalSlot'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; +import { stopCtxPropagation } from '../utils/UtilityFunctions'; + +/** + * 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', 'internal compartments'); + + this._empty = this._empty.bind(this); + this._fillWithCargo = this._fillWithCargo.bind(this); + this._fillWithCells = this._fillWithCells.bind(this); + this._fillWithArmor = this._fillWithArmor.bind(this); + } + + /** + * Empty all slots + */ + _empty() { + this.props.ship.emptyInternal(); + this.props.onChange(); + this._close(); + } + + /** + * Fill all slots with cargo racks + * @param {SyntheticEvent} event Event + */ + _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(); + } + + /** + * Fill all slots with Shield Cell Banks + * @param {SyntheticEvent} event Event + */ + _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(); + } + + /** + * Fill all slots with Hull Reinforcement Packages + * @param {SyntheticEvent} event Event + */ + _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(); + } + + /** + * Empty all on section header right click + */ + _contextMenu() { + this._empty(); + } + + /** + * Generate the slot React Components + * @return {Array} Array of Slots + */ + _getSlots() { + let slots = []; + let { currentMenu, ship } = this.props; + let { originSlot, targetSlot } = this.state; + 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, s)} + selected={currentMenu == s} + enabled={s.enabled} + m={s.m} + drag={this._drag.bind(this, s)} + dragOver={this._dragOverSlot.bind(this, s)} + drop={this._drop} + dropClass={this._dropClass(s, originSlot, targetSlot)} + fuel={fuelCapacity} + ship={ship} + />); + } + + return slots; + } + + /** + * Generate the section drop-down menu + * @param {Function} translate Translate function + * @return {React.Component} Section menu + */ + _getSectionMenu(translate) { + return
    e.stopPropagation()} onContextMenu={stopCtxPropagation}> +
      +
    • {translate('empty all')}
    • +
    • {translate('cargo')}
    • +
    • {translate('scb')}
    • +
    • {translate('hr')}
    • +
    +
    ; + } + +} diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx new file mode 100644 index 00000000..15823ec7 --- /dev/null +++ b/src/app/components/LineChart.jsx @@ -0,0 +1,268 @@ +import React from 'react'; +import d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; + +const RENDER_POINTS = 20; // Only render 20 points on the graph +const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; + +/** + * Line Chart + */ +export default class LineChart extends TranslatedComponent { + + static defaultProps = { + xMin: 0, + yMin: 0, + colors: ['#ff8c0d'] + }; + + static PropTypes = { + width: React.PropTypes.number.isRequired, + func: React.PropTypes.func.isRequired, + xLabel: React.PropTypes.string.isRequired, + xMin: React.PropTypes.number, + xMax: React.PropTypes.number.isRequired, + xUnit: React.PropTypes.string.isRequired, + yLabel: React.PropTypes.string.isRequired, + yMin: React.PropTypes.number, + yMax: React.PropTypes.number.isRequired, + yUnit: React.PropTypes.string.isRequired, + series: React.PropTypes.array, + colors: React.PropTypes.array, + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._updateDimensions = this._updateDimensions.bind(this); + this._updateSeriesData = this._updateSeriesData.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); + + let markerElems = []; + let detailElems = []; + let xScale = d3.scale.linear(); + let xAxisScale = d3.scale.linear(); + let yScale = d3.scale.linear(); + let series = props.series; + let seriesLines = []; + + this.xAxis = d3.svg.axis().scale(xAxisScale).outerTickSize(0).orient('bottom'); + this.yAxis = d3.svg.axis().scale(yScale).ticks(6).outerTickSize(0).orient('left'); + + for(let i = 0, l = series ? series.length : 1; i < l; i++) { + let yAccessor = series ? function(d) { return yScale(d[1][this]); }.bind(series[i]) : (d) => yScale(d[1]); + seriesLines.push(d3.svg.line().x((d) => xScale(d[0])).y(yAccessor)); + detailElems.push(); + markerElems.push(); + } + + this.state = { + xScale, + xAxisScale, + yScale, + seriesLines, + detailElems, + markerElems, + tipHeight: 2 + (1.2 * (series ? series.length : 0.8)) + }; + } + + /** + * Update tooltip content + * @param {number} xPos x coordinate + */ + _tooltip(xPos) { + let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; + let { xScale, yScale, innerWidth } = this.state; + let { formats, translate } = this.context.language; + let x0 = xScale.invert(xPos), + y0 = func(x0), + tips = this.tipContainer, + yTotal = 0, + flip = (xPos / innerWidth > 0.60), + tipWidth = 0, + tipHeightPx = tips.selectAll('rect').node().getBoundingClientRect().height; + + + xPos = xScale(x0); // Clamp xPos + + tips.selectAll('text.text-tip.y').text(function(d, i) { + let yVal = series ? y0[series[i]] : y0; + yTotal += yVal; + return (series ? translate(series[i]) : '') + ' ' + formats.f2(yVal); + }).append('tspan').attr('class', 'metric').text(' ' + yUnit); + + tips.selectAll('text').each(function() { + if (this.getBBox().width > tipWidth) { + tipWidth = Math.ceil(this.getBBox().width); + } + }); + + let tipY = Math.floor(yScale(yTotal / (series ? series.length : 1)) - (tipHeightPx / 2)); + + tipWidth += 8; + tips.attr('transform', 'translate(' + xPos + ',' + tipY + ')'); + tips.selectAll('text.text-tip').attr('x', flip ? -12 : 12).style('text-anchor', flip ? 'end' : 'start'); + tips.selectAll('text.text-tip.x').text(formats.f2(x0)).append('tspan').attr('class', 'metric').text(' ' + xUnit); + tips.selectAll('rect').attr('width', tipWidth + 4).attr('x', flip ? -tipWidth - 12 : 8).attr('y', 0).style('text-anchor', flip ? 'end' : 'start'); + this.markersContainer.selectAll('circle').attr('cx', xPos).attr('cy', (d, i) => yScale(series ? y0[series[i]] : y0)); + } + + /** + * Update dimensions based on properties and scale + * @param {Object} props React Component properties + * @param {number} scale size ratio / scale + */ + _updateDimensions(props, scale) { + let { width, xMax, xMin, yMin, yMax } = props; + let innerWidth = width - MARGIN.left - MARGIN.right; + let outerHeight = Math.round(width * 0.5 * scale); + let innerHeight = outerHeight - MARGIN.top - MARGIN.bottom; + + this.state.xScale.range([0, innerWidth]).domain([xMin, xMax || 1]).clamp(true); + this.state.xAxisScale.range([0, innerWidth]).domain([xMin, xMax]).clamp(true); + this.state.yScale.range([innerHeight, 0]).domain([yMin, yMax]); + this.setState({ innerWidth, outerHeight, innerHeight }); + } + + /** + * Show tooltip + * @param {SyntheticEvent} e Event + */ + _showTip(e) { + e.preventDefault(); + this.tipContainer.style('display', null); + this.markersContainer.style('display', null); + this._moveTip(e); + } + + /** + * Move and update tooltip + * @param {SyntheticEvent} e Event + */ + _moveTip(e) { + let clientX = e.touches ? e.touches[0].clientX : e.clientX; + this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left)); + } + + /** + * Hide tooltip + * @param {SyntheticEvent} e Event + */ + _hideTip(e) { + e.preventDefault(); + this.tipContainer.style('display', 'none'); + this.markersContainer.style('display', 'none'); + } + + /** + * Update series data generated from props + * @param {Object} props React Component properties + */ + _updateSeriesData(props) { + let { func, xMin, xMax, series } = props; + let delta = (xMax - xMin) / RENDER_POINTS; + let seriesData = new Array(RENDER_POINTS); + + if (delta) { + seriesData = new Array(RENDER_POINTS); + for (let i = 0, x = xMin; i < RENDER_POINTS; i++) { + seriesData[i] = [x, func(x)]; + x += delta; + } + seriesData[RENDER_POINTS - 1] = [xMax, func(xMax)]; + } else { + let yVal = func(xMin); + seriesData = [[0, yVal], [1, yVal]]; + } + + this.setState({ seriesData }); + } + + /** + * Update dimensions and series data based on props and context. + */ + componentWillMount() { + this._updateDimensions(this.props, this.context.sizeRatio); + this._updateSeriesData(this.props); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + let { func, xMin, xMax, yMin, yMax, width } = nextProps; + let props = this.props; + + let domainChanged = xMax != props.xMax || xMin != props.xMin || yMax != props.yMax || yMin != props.yMin || func != props.func; + + if (width != props.width || domainChanged || this.context.sizeRatio != nextContext.sizeRatio) { + this._updateDimensions(nextProps, nextContext.sizeRatio); + } + + if (domainChanged) { + this._updateSeriesData(nextProps); + } + } + + /** + * Render the chart + * @return {React.Component} Chart SVG + */ + render() { + if (!this.props.width) { + return null; + } + + let { xLabel, yLabel, xUnit, yUnit, colors } = this.props; + let { innerWidth, outerHeight, innerHeight, tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + let line = this.line; + let lines = seriesLines.map((line, i) => ); + + return + + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + {` (${xUnit})`} + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + {` (${yUnit})`} + + + this.tipContainer = d3.select(g)} style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + + + ; + } +} diff --git a/src/app/components/Link.jsx b/src/app/components/Link.jsx new file mode 100644 index 00000000..aef5db46 --- /dev/null +++ b/src/app/components/Link.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import Router from '../Router'; +import { shallowEqual } from '../utils/UtilityFunctions'; + +/** + * Link wrapper component + */ +export default class Link extends React.Component { + + static propTypes = { + children: React.PropTypes.any, + href: React.PropTypes.string.isRequired, + onClick: React.PropTypes.func + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this.handler = this.handler.bind(this); + } + + /** + * Determine if a component should be rerendered + * @param {object} nextProps Next properties + * @return {boolean} true if update is needed + */ + shouldComponentUpdate(nextProps) { + return !shallowEqual(this.props, nextProps); + } + + /** + * Link click handler + * @param {SyntheticEvent} event Event + */ + handler(event) { + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey || event.button > 1) { + return; + } + event.preventDefault(); + + if (this.props.onClick) { + this.props.onClick(event); + } else if (this.props.href) { + Router.go(this.props.href); + } + } + + /** + * Renders the link + * @return {React.Component} A href element + */ + render() { + return {this.props.children}; + } + +} \ No newline at end of file diff --git a/src/app/components/ModalCompare.jsx b/src/app/components/ModalCompare.jsx new file mode 100644 index 00000000..e62e3a40 --- /dev/null +++ b/src/app/components/ModalCompare.jsx @@ -0,0 +1,142 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import { Ships } from 'coriolis-data/dist'; +import Persist from '../stores/Persist'; + +/** + * Build ship and name comparator + * @param {Object} a [description] + * @param {Object} b [description] + * @return {number} 1, 0, -1 + */ +function buildComparator(a, b) { + if (a.name == b.name) { + return a.buildName.localeCompare(b.buildName); + } + return a.name.localeCompare(b.name); +} + +/** + * Compare builds modal + */ +export default class ModalCompare extends TranslatedComponent { + + static propTypes = { + onSelect: React.PropTypes.func.isRequired, + builds: React.PropTypes.array + }; + + static defaultProps = { + builds: [] + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + let builds = props.builds; + let allBuilds = Persist.getBuilds(); + let unusedBuilds = []; + let usedBuilds = []; + + for (let id in allBuilds) { + for (let buildName in allBuilds[id]) { + let b = { id, buildName, name: Ships[id].properties.name }; + builds.find((e) => e.buildName == buildName && e.id == id) ? usedBuilds.push(b) : unusedBuilds.push(b); + } + } + + usedBuilds.sort(buildComparator); + unusedBuilds.sort(buildComparator); + + this.state = { usedBuilds, unusedBuilds, used: usedBuilds.length }; + } + + /** + * Add a build to the compare list + * @param {number} buildIndex Idnex of build in list + */ + _addBuild(buildIndex) { + let { usedBuilds, unusedBuilds } = this.state; + usedBuilds.push(unusedBuilds[buildIndex]); + unusedBuilds.splice(buildIndex, 1); + usedBuilds.sort(buildComparator); + + this.setState({ used: usedBuilds.length }); + } + + /** + * Remove a build from the compare list + * @param {number} buildIndex Idnex of build in list + */ + _removeBuild(buildIndex) { + let { usedBuilds, unusedBuilds } = this.state; + unusedBuilds.push(usedBuilds[buildIndex]); + usedBuilds.splice(buildIndex, 1); + unusedBuilds.sort(buildComparator); + + this.setState({ used: usedBuilds.length }); + } + + /** + * OK Action - Use selected builds + */ + _selectBuilds() { + this.props.onSelect(this.state.usedBuilds); + } + + /** + * Render the modal + * @return {React.Component} Modal Content + */ + render() { + let { usedBuilds, unusedBuilds } = this.state; + let translate = this.context.language.translate; + + let availableBuilds = unusedBuilds.map((build, i) => + + {build.name} + {build.buildName} + + ); + + let selectedBuilds = usedBuilds.map((build, i) => + + {build.name}< + td className='tl'>{build.buildName} + + ); + + return
    e.stopPropagation() }> +

    {translate('PHRASE_SELECT_BUILDS')}

    +
    +
    +

    {translate('available')}

    +
    + + + {availableBuilds} + +
    +
    +
    +

    +
    +

    {translate('added')}

    +
    + + + {selectedBuilds} + +
    +
    +
    +
    +
    + + +
    ; + } +} diff --git a/src/app/components/ModalDeleteAll.jsx b/src/app/components/ModalDeleteAll.jsx new file mode 100644 index 00000000..fd0eb948 --- /dev/null +++ b/src/app/components/ModalDeleteAll.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import TranslatedComponent from './TranslatedComponent'; +import Persist from '../stores/Persist'; + +/** + * Delete All saved data modal + */ +export default class ModalDeleteAll extends TranslatedComponent { + + /** + * Delete everything and hide the modal + */ + _deleteAll() { + Persist.deleteAll(); + this.context.hideModal(); + } + + /** + * Renders the component + * @return {React.Component} Modal contents + */ + render() { + let translate = this.context.language.translate; + + return
    e.stopPropagation()}> +

    {translate('delete all')}

    +

    {translate('PHRASE_CONFIRMATION')}

    + + +
    ; + } +} diff --git a/src/app/components/ModalExport.jsx b/src/app/components/ModalExport.jsx new file mode 100644 index 00000000..cfe9c46d --- /dev/null +++ b/src/app/components/ModalExport.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import { findDOMNode } from 'react-dom'; +import TranslatedComponent from './TranslatedComponent'; + +/** + * Export Modal + */ +export default class ModalExport extends TranslatedComponent { + + static propTypes = { + title: React.PropTypes.string, + generator: React.PropTypes.func, + data: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.object, React.PropTypes.array]) + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + let exportJson; + + if (props.generator) { + exportJson = 'Generating...'; + } else if(typeof props.data == 'string') { + exportJson = props.data; + } else { + exportJson = JSON.stringify(this.props.data, null, 2); + } + + this.state = { exportJson }; + } + + /** + * If generator is provided, execute on mount + */ + componentWillMount() { + if (this.props.generator) { + this.props.generator((str) => this.setState({ exportJson: str })); + } + } + + /** + * Focus on textarea and select all + */ + componentDidMount() { + let e = findDOMNode(this.refs.exportField); + if (e) { + e.focus(); + e.select(); + } + } + + /** + * Render the modal + * @return {React.Component} Modal Content + */ + render() { + let translate = this.context.language.translate; + let description; + + if (this.props.description) { + description =
    {translate(this.props.description)}
    ; + } + + return
    e.stopPropagation() }> +

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

    + {description} +
    +