From 14ffa26ef98fda7b3e269c7e5c32795754f14b7b Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Thu, 16 May 2024 19:23:33 +0100 Subject: [PATCH 1/8] Adds valid module checking to all types of modules on import --- src/app/components/AvailableModulesMenu.jsx | 27 +- src/app/components/HardpointSlot.jsx | 1 + src/app/components/InternalSlot.jsx | 1 + src/app/components/Slot.jsx | 15 +- src/app/components/StandardSlot.jsx | 9 +- src/app/i18n/en.json | 4 + src/app/pages/ErrorDetails.jsx | 3 + src/app/shipyard/Module.js | 9 + src/app/shipyard/ModuleUtils.js | 9 + src/app/utils/BlueprintFunctions.js | 870 ++++++++++---------- src/app/utils/JournalUtils.js | 123 ++- 11 files changed, 610 insertions(+), 461 deletions(-) diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index de4a6e17..110953cd 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -213,16 +213,30 @@ export default class AvailableModulesMenu extends TranslatedComponent { if (categories.length === 1) { // Show category header instead of group header if (m && grp == m.grp) { - list.push(
this.groupElem = elem} key={category} + // If this is a missing module/weapon, skip it + if (m.grp == "mh" || m.grp == "mm"){ + continue; + } else { + list.push(
this.groupElem = elem} key={category} className={'select-category upp'}>{translate(category)}
); + } } else { - list.push(
{translate(category)}
); + if (category == "mh"){ + continue; + } else { + list.push(
{translate(category)}
); + } } } else { // Show category header as well as group header if (!categoryHeader) { - list.push(
{translate(category)}
); - categoryHeader = true; + if (category == "mh"){ + continue; + } + else { + list.push(
{translate(category)}
); + categoryHeader = true; + } } if (m && grp == m.grp) { list.push(
this.groupElem = elem} key={grp} @@ -298,6 +312,10 @@ export default class AvailableModulesMenu extends TranslatedComponent { let itemsOnThisRow = 0; for (let i = 0; i < sortedModules.length; i++) { let m = sortedModules[i]; + if (ModuleUtils.isMissingModule(m.info)) { + // If this is a missing module, skip it + continue; + } let mount = null; let disabled = false; prevName = m.name; @@ -312,6 +330,7 @@ export default class AvailableModulesMenu extends TranslatedComponent { disabled = 1 <= ship.internal.filter(o => o.m && o.m.grp === 'mlc').length; } let active = mountedModule && mountedModule.id === m.id; + let classes = cn(m.name ? 'lc' : 'c', { warning: !disabled && warningFunc && warningFunc(m), active, diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 724d9fd5..e9edda43 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -136,6 +136,7 @@ export default class HardpointSlot extends Slot { {showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null} {m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null} + {m.getInfo() ?
{translate(m.getInfo())}
: null} {m && validMods.length > 0 ?
this.modButton = modButton}>
: null }
; diff --git a/src/app/components/Slot.jsx b/src/app/components/Slot.jsx index 8c04dbab..1594cad6 100644 --- a/src/app/components/Slot.jsx +++ b/src/app/components/Slot.jsx @@ -99,6 +99,7 @@ export default class Slot extends TranslatedComponent { let translate = language.translate; let { ship, m, enabled, dropClass, dragOver, onOpen, onChange, selected, eligible, onSelect, warning, availableModules } = this.props; let slotDetails, modificationsMarker, menu; + let missing = false; if (!selected) { // If not selected then sure that modifications flag is unset @@ -108,6 +109,11 @@ export default class Slot extends TranslatedComponent { if (m) { slotDetails = this._getSlotDetails(m, enabled, translate, language.formats, language.units); // Must be implemented by sub classes modificationsMarker = JSON.stringify(m); + if(typeof m.grp !== 'undefined' || m.grp !== null) { + if(m.grp == "mh" || m.grp == "mm") { + missing = true; + } + } } else { slotDetails =
{translate(eligible ? 'emptyrestricted' : 'empty')}
; modificationsMarker = ''; @@ -138,13 +144,16 @@ export default class Slot extends TranslatedComponent { } // TODO: implement touch dragging - + return (
this.slotDiv = slotDiv}> -
+ { + // If missing module/hardpoint, set the div container to warning status. + } +
{this._getMaxClassLabel(translate)}
{slotDetails} -
+
{menu}
); diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 1e810982..70bb57ae 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -93,6 +93,10 @@ export default class StandardSlot extends TranslatedComponent { this._modificationsSelected = false; } + if (m.info) { + warning = () => true; + } + const modificationsMarker = JSON.stringify(m); if (selected) { @@ -124,7 +128,7 @@ export default class StandardSlot extends TranslatedComponent {
{m.grp == 'bh' ? m.name.charAt(0) : slot.maxClass}
-
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
+
{classRating} {m.getInfo() ? translate(m.ukName) : translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
{formats.round(mass)}{units.T}
@@ -144,7 +148,8 @@ export default class StandardSlot extends TranslatedComponent { { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } { m.getIntegrity() ?
{translate('integrity')}: {formats.int(m.getIntegrity())}
: null } - { validMods.length > 0 ?
this.modButton = modButton }>
: null } + { m.getInfo() ?
{translate(m.getInfo())}
: null } + { m.getInfo() ?
: validMods.length > 0 ?
this.modButton = modButton }>
: null }
diff --git a/src/app/i18n/en.json b/src/app/i18n/en.json index fd6b3f0b..59beffb2 100644 --- a/src/app/i18n/en.json +++ b/src/app/i18n/en.json @@ -83,6 +83,7 @@ "HELP_MODIFICATIONS_MENU": "Click on a number to enter a new value, or drag along the bar for small changes", "PHRASE_FAIL_EDENGINEER": "Failed to send to EDEngineer (Launch EDEngineer and make sure the API is started then refresh the page.)", "PHRASE_FIREFOX_EDENGINEER": "Sending to EDEngineer is not compatible with Firefox's security settings. Please try again with Chrome.", + "MISSING_MODULES": "Missing Modules", "am": "Auto Field-Maintenance Unit", "bh": "Bulkheads", "bl": "Beam Laser", @@ -109,6 +110,8 @@ "kw": "Kill Warrant Scanner", "ls": "Life Support", "mc": "Multi-cannon", + "mh": "Missing Weapon/Utility", + "mm": "Missing Module", "axmc": "AX Multi-cannon", "ml": "Mining Laser", "mlc": "Multi Limpet Controller", @@ -212,6 +215,7 @@ "boost interval": "Boost interval", "total": "Total", "ammo": "Ammunition maximum", + "info": "Info", "boot": "Boot time", "hacktime": "Hack time", "brokenregen": "Broken regeneration rate", diff --git a/src/app/pages/ErrorDetails.jsx b/src/app/pages/ErrorDetails.jsx index 5c68b8c9..4d96f93e 100644 --- a/src/app/pages/ErrorDetails.jsx +++ b/src/app/pages/ErrorDetails.jsx @@ -45,6 +45,9 @@ export default class ErrorDetails extends React.Component { return

Jameson, we have a problem..

{error.message}

+ Import Error handling has been improved, but still isn't perfect.
MOST Import failures are a result of missing modules in Coriolis,
OR incorrect import strings generated by third party apps. If you're seeing this page, we may have failed to handle the errors in your import correctly. Please see the data output below, specifically the 'scriptUrl:' section if it's there and then if you feel confident enough, please check the github issues page linked below and see if there is a similar issue already logged. If not, please create a new issue with the data below. If you're not confident, please ask for help on the Coriolis Channel of the EDCD Discord server. +
+

{importerror ?
If you are attempting to import a ship from EDDI or EDMC and are seeing a 'Z_BUF_ERROR' it means that the URL has not been provided correctly. This is a common problem when using Microsoft Internet Explorer or Microsoft Edge, and you should use another browser instead.
: null }
diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index dbbdc09c..7df7f44e 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -439,6 +439,15 @@ export default class Module { return this.get('integrity', modified); } + /** + * Get the info of this module + * @param {Boolean} [modified=false] Whether to take modifications into account + * @return {String} the info of this module + */ + getInfo(modified = false) { + return (modified && this.getModValue('info')) || this.info; + } + /** * Get the mass of this module * @param {Boolean} [modified=true] Whether to take modifications into account diff --git a/src/app/shipyard/ModuleUtils.js b/src/app/shipyard/ModuleUtils.js index 276de3c1..209e24b7 100755 --- a/src/app/shipyard/ModuleUtils.js +++ b/src/app/shipyard/ModuleUtils.js @@ -322,6 +322,15 @@ export function isShieldGenerator(g) { return g == 'sg' || g == 'psg' || g == 'bsg'; } +/** + * Determine if a module group is a missing module + * @param {String} info Module Group name + * @return {Boolean} True if the group is a missing module + */ +export function isMissingModule(info) { + return info == 'Not in Coriolis yet. Check GitHub issues. Add Issue if needed.'; +} + /** * Creates a new ModuleSet that contains all available modules * that the specified ship is eligible to use. diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index 80259bea..3365c05c 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -1,435 +1,435 @@ -import React from 'react'; -import { Modifications } from 'coriolis-data/dist'; -import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; - -/** - * Generate a tooltip with details of a blueprint's specials - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @param {string} specialName The name of the special - * @returns {Object} The react components - */ -export function specialToolTip(translate, blueprint, grp, m, specialName) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - if (m) { - // We also add in any benefits from specials that aren't covered above - if (m.blueprint) { - for (const feature in Modifications.modifierActions[specialName]) { - // if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature) - m.getModValue(feature, true); - if (featureDef.type === 'percentage') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - - return ( -
- - - {effects} - -
-
- ); -} - -/** - * Generate a tooltip with details of a blueprint's effects - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {Array} engineers The engineers supplying this blueprint - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @returns {Object} The react components - */ -export function blueprintTooltip(translate, blueprint, engineers, grp, m) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - for (const feature in blueprint.features) { - const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); - const featureDef = Modifications.modifications[feature]; - if (!featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let lowerBound = blueprint.features[feature][0]; - let upperBound = blueprint.features[feature][1]; - if (featureDef.type === 'percentage') { - lowerBound = Math.round(lowerBound * 1000) / 10; - upperBound = Math.round(upperBound * 1000) / 10; - } - const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); - const upperIsBeneficial = isValueBeneficial(feature, upperBound); - if (m) { - // We have a module - add in the current value - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {current}{symbol} - {upperBound}{symbol} - - ); - } else { - // We do not have a module, no value - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {upperBound}{symbol} - - ); - } - } - } - if (m) { - // Because we have a module add in any benefits that aren't part of the primary blueprint - for (const feature in m.mods) { - if (!blueprint.features[feature]) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - - // We also add in any benefits from specials that aren't covered above - if (m.blueprint && m.blueprint.special) { - for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { - if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - } - - let components; - if (!m) { - components = []; - for (const component in blueprint.components) { - components.push( - - {translate(component)} - {blueprint.components[component]} - - ); - } - } - - let engineersList; - if (engineers) { - engineersList = []; - for (const engineer of engineers) { - engineersList.push( - - {engineer} - - ); - } - } - - return ( -
- - - - - - {m ? : null } - - - - - {effects} - -
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
- { components ? - - - - - - - - {components} - -
{translate('component')}{translate('amount')}
: null } - { engineersList ? - - - - - - - {engineersList} - -
{translate('engineers')}
: null } -
- ); -} - -/** - * Is this blueprint feature beneficial? - * @param {string} feature The name of the feature - * @param {array} values The value of the feature - * @returns {boolean} True if this feature is beneficial - */ -export function isBeneficial(feature, values) { - const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); - if (Modifications.modifications[feature].higherbetter) { - return !fact; - } else { - return fact; - } -} - -/** - * Is this feature value beneficial? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature - * @returns {boolean} True if this value is beneficial - */ -export function isValueBeneficial(feature, value) { - if (Modifications.modifications[feature].higherbetter) { - return value > 0; - } else { - return value < 0; - } -} - -/** - * Is the change as shown beneficial? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature as percentage change - * @returns True if the value is beneficial - */ -export function isChangeValueBeneficial(feature, value) { - let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; - if (changeHigherBetter === undefined) { - return isValueBeneficial(feature, value); - } - - if (changeHigherBetter) { - return value > 0; - } else { - return value < 0; - } -} - -/** - * Get a blueprint with a given name and an optional module - * @param {string} name The name of the blueprint - * @param {Object} module The module for which to obtain this blueprint - * @returns {Object} The matching blueprint - */ -export function getBlueprint(name, module) { - // Start with a copy of the blueprint - const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); - const found = Modifications.blueprints[findMod(name)]; - if (!found || !found.fdname) { - return {}; - } - const blueprint = JSON.parse(JSON.stringify(found)); - return blueprint; -} - -/** - * Provide 'percent' primary modifications - * @param {Object} ship The ship for which to perform the modifications - * @param {Object} m The module for which to perform the modifications - * @param {Number} percent The percent to set values to of full. - */ -export function setPercent(ship, m, percent) { - ship.clearModifications(m); - // Pick given value as multiplier - const mult = percent / 100; - setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); -} - -/** - * Sets the blueprint quality and fires a callback for each property affected. - * @param {Object} blueprint The ship for which to perform the modifications - * @param {Number} quality The quality to apply - float number 0 to 1. - * @param {Function} cb The Callback to run for each property. Function (featureName, value) - */ -export function setQualityCB(blueprint, quality, cb) { - // Pick given value as multiplier - const features = blueprint.grades[blueprint.grade].features; - for (const featureName in features) { - let value; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } else { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } - } else { - // Higher is worse, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } else { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } - } - - if (Modifications.modifications[featureName].type == 'percentage') { - value = value * 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - value = value * 100; - } - - cb(featureName, value); - } -} - -/** - * Provide 'random' primary modifications - * @param {Object} ship The ship for which to perform the modifications - * @param {Object} m The module for which to perform the modifications - */ -export function setRandom(ship, m) { - // Pick a single value for our randomness - setPercent(ship, m, Math.random() * 100); -} - -/** - * Provide 'percent' primary query - * @param {Object} m The module for which to perform the query - * @returns {Number} percent The percentage indicator of current applied values. - */ -export function getPercent(m) { - let result = null; - const features = m.blueprint.grades[m.blueprint.grade].features; - for (const featureName in features) { - if (features[featureName][0] === features[featureName][1]) { - continue; - } - - let value = _getValue(m, featureName); - let mult; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); - } else { - mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); - } - } else { - // Higher is worse, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); - } else { - mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); - } - } - - if (result && result != mult) { - return null; - } else if (result != mult) { - result = mult; - } - } - - return result; -} - -/** - * Query a feature value - * @param {Object} m The module for which to perform the query - * @param {string} featureName The feature being queried - * @returns {number} The value of the modification as a % - */ -function _getValue(m, featureName) { - if (Modifications.modifications[featureName].type == 'percentage') { - return m.getModValue(featureName, true) / 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - return m.getModValue(featureName, true) / 100; - } else { - return m.getModValue(featureName, true); - } -} +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; +import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; + +/** + * Generate a tooltip with details of a blueprint's specials + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @param {string} specialName The name of the special + * @returns {Object} The react components + */ +export function specialToolTip(translate, blueprint, grp, m, specialName) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + if (m) { + // We also add in any benefits from specials that aren't covered above + if (m.blueprint) { + for (const feature in Modifications.modifierActions[specialName]) { + // if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature) - m.getModValue(feature, true); + if (featureDef.type === 'percentage') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + + return ( +
+ + + {effects} + +
+
+ ); +} + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {Array} engineers The engineers supplying this blueprint + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, blueprint, engineers, grp, m) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + for (const feature in blueprint.features) { + const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = blueprint.features[feature][0]; + let upperBound = blueprint.features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); + const upperIsBeneficial = isValueBeneficial(feature, upperBound); + if (m) { + // We have a module - add in the current value + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {current}{symbol} + {upperBound}{symbol} + + ); + } else { + // We do not have a module, no value + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {upperBound}{symbol} + + ); + } + } + } + if (m) { + // Because we have a module add in any benefits that aren't part of the primary blueprint + for (const feature in m.mods) { + if (!blueprint.features[feature]) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + + // We also add in any benefits from specials that aren't covered above + if (m.blueprint && m.blueprint.special) { + for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { + if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + } + + let components; + if (!m) { + components = []; + for (const component in blueprint.components) { + components.push( + + {translate(component)} + {blueprint.components[component]} + + ); + } + } + + let engineersList; + if (engineers) { + engineersList = []; + for (const engineer of engineers) { + engineersList.push( + + {engineer} + + ); + } + } + + return ( +
+ + + + + + {m ? : null } + + + + + {effects} + +
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
+ { components ? + + + + + + + + {components} + +
{translate('component')}{translate('amount')}
: null } + { engineersList ? + + + + + + + {engineersList} + +
{translate('engineers')}
: null } +
+ ); +} + +/** + * Is this blueprint feature beneficial? + * @param {string} feature The name of the feature + * @param {array} values The value of the feature + * @returns {boolean} True if this feature is beneficial + */ +export function isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } +} + +/** + * Is this feature value beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature + * @returns {boolean} True if this value is beneficial + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Is the change as shown beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature as percentage change + * @returns True if the value is beneficial + */ +export function isChangeValueBeneficial(feature, value) { + let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; + if (changeHigherBetter === undefined) { + return isValueBeneficial(feature, value); + } + + if (changeHigherBetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Get a blueprint with a given name and an optional module + * @param {string} name The name of the blueprint + * @param {Object} module The module for which to obtain this blueprint + * @returns {Object} The matching blueprint + */ +export function getBlueprint(name, module) { + // Start with a copy of the blueprint + const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); + const found = Modifications.blueprints[findMod(name)]; + if (!found || !found.fdname) { + return {}; + } + const blueprint = JSON.parse(JSON.stringify(found)); + return blueprint; +} + +/** + * Provide 'percent' primary modifications + * @param {Object} ship The ship for which to perform the modifications + * @param {Object} m The module for which to perform the modifications + * @param {Number} percent The percent to set values to of full. + */ +export function setPercent(ship, m, percent) { + ship.clearModifications(m); + // Pick given value as multiplier + const mult = percent / 100; + setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); +} + +/** + * Sets the blueprint quality and fires a callback for each property affected. + * @param {Object} blueprint The ship for which to perform the modifications + * @param {Number} quality The quality to apply - float number 0 to 1. + * @param {Function} cb The Callback to run for each property. Function (featureName, value) + */ +export function setQualityCB(blueprint, quality, cb) { + // Pick given value as multiplier + const features = blueprint.grades[blueprint.grade].features; + for (const featureName in features) { + let value; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } else { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } + } else { + // Higher is worse, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } else { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } + } + + if (Modifications.modifications[featureName].type == 'percentage') { + value = value * 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + value = value * 100; + } + + cb(featureName, value); + } +} + +/** + * Provide 'random' primary modifications + * @param {Object} ship The ship for which to perform the modifications + * @param {Object} m The module for which to perform the modifications + */ +export function setRandom(ship, m) { + // Pick a single value for our randomness + setPercent(ship, m, Math.random() * 100); +} + +/** + * Provide 'percent' primary query + * @param {Object} m The module for which to perform the query + * @returns {Number} percent The percentage indicator of current applied values. + */ +export function getPercent(m) { + let result = null; + const features = m.blueprint.grades[m.blueprint.grade].features; + for (const featureName in features) { + if (features[featureName][0] === features[featureName][1]) { + continue; + } + + let value = _getValue(m, featureName); + let mult; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); + } else { + mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); + } + } else { + // Higher is worse, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); + } else { + mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); + } + } + + if (result && result != mult) { + return null; + } else if (result != mult) { + result = mult; + } + } + + return result; +} + +/** + * Query a feature value + * @param {Object} m The module for which to perform the query + * @param {string} featureName The feature being queried + * @returns {number} The value of the modification as a % + */ +function _getValue(m, featureName) { + if (Modifications.modifications[featureName].type == 'percentage') { + return m.getModValue(featureName, true) / 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + return m.getModValue(featureName, true) / 100; + } else { + return m.getModValue(featureName, true); + } +} diff --git a/src/app/utils/JournalUtils.js b/src/app/utils/JournalUtils.js index bb78993d..5b7cf0f5 100644 --- a/src/app/utils/JournalUtils.js +++ b/src/app/utils/JournalUtils.js @@ -6,6 +6,22 @@ import { Modules } from 'coriolis-data/dist'; import { Modifications } from 'coriolis-data/dist'; import { getBlueprint, setQualityCB } from './BlueprintFunctions'; +/** + * Check if an imported module is valid + * @param {Object} module the module to check + * @param {Object} moduleType the type of module to check + * @return {boolean} true if the module is valid + */ +function _isValidImportedModule(module, moduleType) { + // First of all, has the _moduleFromFdName function returned 'null'? + if (!module){ + return false + } + else { + return true + } +} + /** * Obtain a module given its FD Name * @param {string} fdname the FD Name of the module @@ -98,49 +114,90 @@ export function shipFromLoadoutJSON(json) { if (module.Engineering) _addModifications(ship.bulkheads.m, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'powerplant': - const powerplant = _moduleFromFdName(module.Item); + let powerplant = _moduleFromFdName(module.Item); + // Check the powerplant returned is valid + if (!_isValidImportedModule(powerplant, 'powerplant')) + { + powerplant = _moduleFromFdName('Int_Missing_Powerplant'); + module.Engineering = null; + } ship.use(ship.standard[0], powerplant, true); ship.standard[0].enabled = module.On; ship.standard[0].priority = module.Priority; if (module.Engineering) _addModifications(powerplant, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'mainengines': - const thrusters = _moduleFromFdName(module.Item); + let thrusters = _moduleFromFdName(module.Item); + // Check the thrusters returned is valid + if (!_isValidImportedModule(thrusters, 'thrusters')) + { + thrusters = _moduleFromFdName('Int_Missing_Engine'); + module.Engineering = null; + } ship.use(ship.standard[1], thrusters, true); ship.standard[1].enabled = module.On; ship.standard[1].priority = module.Priority; if (module.Engineering) _addModifications(thrusters, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'frameshiftdrive': - const frameshiftdrive = _moduleFromFdName(module.Item); + let frameshiftdrive = _moduleFromFdName(module.Item); + // Check the frameshiftdrive returned is valid + if (!_isValidImportedModule(frameshiftdrive, 'frameshiftdrive')) + { + frameshiftdrive = _moduleFromFdName('Int_Missing_Hyperdrive'); + module.Engineering = null; + } ship.use(ship.standard[2], frameshiftdrive, true); ship.standard[2].enabled = module.On; ship.standard[2].priority = module.Priority; if (module.Engineering) _addModifications(frameshiftdrive, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'lifesupport': - const lifesupport = _moduleFromFdName(module.Item); + let lifesupport = _moduleFromFdName(module.Item); + // Check the lifesupport returned is valid + if (!_isValidImportedModule(lifesupport, 'lifesupport')) + { + lifesupport = _moduleFromFdName('Int_Missing_LifeSupport'); + module.Engineering = null; + } ship.use(ship.standard[3], lifesupport, true); ship.standard[3].enabled = module.On === true; ship.standard[3].priority = module.Priority; if (module.Engineering) _addModifications(lifesupport, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'powerdistributor': - const powerdistributor = _moduleFromFdName(module.Item); + let powerdistributor = _moduleFromFdName(module.Item); + // Check the powerdistributor returned is valid + if (!_isValidImportedModule(powerdistributor, 'powerdistributor')) + { + powerdistributor = _moduleFromFdName('Int_Missing_PowerDistributor'); + module.Engineering = null; + } ship.use(ship.standard[4], powerdistributor, true); ship.standard[4].enabled = module.On; ship.standard[4].priority = module.Priority; if (module.Engineering) _addModifications(powerdistributor, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'radar': - const sensors = _moduleFromFdName(module.Item); + let sensors = _moduleFromFdName(module.Item); + // Check the sensors returned is valid + if (!_isValidImportedModule(sensors, 'sensors')) + { + sensors = _moduleFromFdName('Int_Missing_Sensors'); + module.Engineering = null; + } ship.use(ship.standard[5], sensors, true); ship.standard[5].enabled = module.On; ship.standard[5].priority = module.Priority; if (module.Engineering) _addModifications(sensors, module.Engineering.Modifiers, module.Engineering.Quality, module.Engineering.BlueprintName, module.Engineering.Level, module.Engineering.ExperimentalEffect); break; case 'fueltank': - const fueltank = _moduleFromFdName(module.Item); + let fueltank = _moduleFromFdName(module.Item); + // Check the fueltank returned is valid + if (!_isValidImportedModule(fueltank, 'fueltank')) + { + fueltank = _moduleFromFdName('Int_Missing_FuelTank'); + } ship.use(ship.standard[6], fueltank, true); ship.standard[6].enabled = true; ship.standard[6].priority = 0; @@ -170,10 +227,27 @@ export function shipFromLoadoutJSON(json) { // This can happen with old imports that don't contain new hardpoints } else { hardpoint = _moduleFromFdName(hardpointSlot.Item); - ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true); - ship.hardpoints[hardpointArrayNum].enabled = hardpointSlot.On; - ship.hardpoints[hardpointArrayNum].priority = hardpointSlot.Priority; - modsToAdd.push({ coriolisMod: hardpoint, json: hardpointSlot }); + // Check the hardpoint module returned is valid + if (!_isValidImportedModule(hardpoint, 'hardpoint')){ + // Check if it's a Utility or Hardpoint + if (hardpointSlot.Slot.toLowerCase().search(/tiny/)) + { + // Use the missing_hardpoint module 'Missing Hardpoint' which will inform the user that the module is missing + hardpoint = _moduleFromFdName('Hpt_Missing_Hardpoint'); + } + else { + // Use the missing_hardpoint module 'Missing Utility' which will inform the user that the module is missing + hardpoint = _moduleFromFdName('Hpt_Missing_Utility'); + } + ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true); + ship.hardpoints[hardpointArrayNum].enabled = hardpointSlot.On; + ship.hardpoints[hardpointArrayNum].priority = hardpointSlot.Priority; + } else { + ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true); + ship.hardpoints[hardpointArrayNum].enabled = hardpointSlot.On; + ship.hardpoints[hardpointArrayNum].priority = hardpointSlot.Priority; + modsToAdd.push({ coriolisMod: hardpoint, json: hardpointSlot }); + } } hardpointArrayNum++; } @@ -187,13 +261,17 @@ export function shipFromLoadoutJSON(json) { continue; } const isMilitary = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].name == 'Military' : false; + const isPlanetary = isNaN(shipTemplate.slots.internal[i]) ? shipTemplate.slots.internal[i].name == 'PlanetaryApproachSuite' : false; - // The internal slot might be a standard or a military slot. Military slots have a different naming system + // The internal slot might be a standard or a military slot, or a planetary slot. Military and Planetary slots have a different naming system let internalSlot = null; if (isMilitary) { const internalName = 'Military0' + militarySlotNum; internalSlot = json.Modules.find(elem => elem.Slot.toLowerCase() === internalName.toLowerCase()); militarySlotNum++; + } else if (isPlanetary) { + const internalName = 'PlanetaryApproachSuite'; + internalSlot = json.Modules.find(elem => elem.Slot.toLowerCase() === internalName.toLowerCase()); } else { // Slot numbers are not contiguous so handle skips. for (; internalSlot === null && internalSlotNum < 99; internalSlotNum++) { @@ -212,11 +290,22 @@ export function shipFromLoadoutJSON(json) { // This can happen with old imports that don't contain new slots } else { const internalJson = internalSlot; - const internal = _moduleFromFdName(internalJson.Item); - ship.use(ship.internal[i], internal, true); - ship.internal[i].enabled = internalJson.On === true; - ship.internal[i].priority = internalJson.Priority; - modsToAdd.push({ coriolisMod: internal, json: internalSlot }); + let internal = _moduleFromFdName(internalJson.Item); + // Check the internal module returned is valid + if (!_isValidImportedModule(internal, 'internal')) + { + internal = _moduleFromFdName('Int_Missing_Module'); + ship.use(ship.internal[i], internal, true); + ship.internal[i].enabled = internalJson.On === true; + ship.internal[i].priority = internalJson.Priority; + //throw 'Unknown internal module: "' + module.Item + '"'; + } + else { + ship.use(ship.internal[i], internal, true); + ship.internal[i].enabled = internalJson.On === true; + ship.internal[i].priority = internalJson.Priority; + modsToAdd.push({ coriolisMod: internal, json: internalSlot }); + } } } From 5c8ff57d16448ffd8a805f29ecdf9485ab6ac9ff Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 17 May 2024 15:24:29 +0100 Subject: [PATCH 2/8] Changes as per comments on the PR --- src/app/components/AvailableModulesMenu.jsx | 2 +- src/app/components/StandardSlot.jsx | 1 + src/app/shipyard/ModuleUtils.js | 9 --------- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index 110953cd..3f2637c8 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -312,7 +312,7 @@ export default class AvailableModulesMenu extends TranslatedComponent { let itemsOnThisRow = 0; for (let i = 0; i < sortedModules.length; i++) { let m = sortedModules[i]; - if (ModuleUtils.isMissingModule(m.info)) { + if (m.grp == 'mh' || m.grp == 'mm') { // If this is a missing module, skip it continue; } diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 70bb57ae..09819009 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -93,6 +93,7 @@ export default class StandardSlot extends TranslatedComponent { this._modificationsSelected = false; } + // If this is a missing module, therefore has the 'info' field, set the warning value on the module to be true when loaded. if (m.info) { warning = () => true; } diff --git a/src/app/shipyard/ModuleUtils.js b/src/app/shipyard/ModuleUtils.js index 209e24b7..276de3c1 100755 --- a/src/app/shipyard/ModuleUtils.js +++ b/src/app/shipyard/ModuleUtils.js @@ -322,15 +322,6 @@ export function isShieldGenerator(g) { return g == 'sg' || g == 'psg' || g == 'bsg'; } -/** - * Determine if a module group is a missing module - * @param {String} info Module Group name - * @return {Boolean} True if the group is a missing module - */ -export function isMissingModule(info) { - return info == 'Not in Coriolis yet. Check GitHub issues. Add Issue if needed.'; -} - /** * Creates a new ModuleSet that contains all available modules * that the specified ship is eligible to use. From f885fde04f07dacf3a7d7154551c57c898a2d2b1 Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 24 May 2024 16:03:03 +0100 Subject: [PATCH 3/8] Fix changed files issue (#3) * Copied de.js contents to new file de-fix.js * Copied de.js contents back from de-fix.js * Copied contents of ko.js to ko-fix.js * Copied ko.js contents back from ko-fix.js * Copied contents from BlueprintFunctions.js to BlueprintFunctions-fix.js * Copied contents back from BlueprintFunctions-fix.js to BlueprintFunctions.js * Copied contents of LineChart.jsx to LineChart-fix.jsx * Copied contents back from LineChart-fix.jsx to LineChart.jsx * Copied contents of PieChart.jsx to PieChart-fix.jsx * Copied contents back from PieChart-fix.jsx to PieChart.jsx * Copied contents from Slider.jsx to Slider-fix.jsx * Copied contents back from Slider-fix.jsx to Slider.jsx * Copied contents from VerticalBarChart.jsx to VerticalBarChart-fix.jsx * Copied contents back from VerticalBarChart-fix.jsx to VerticalBarChart.jsx * Deleting 'fix' files --- src/app/components/LineChart.jsx | 562 +++++++-------- src/app/components/PieChart.jsx | 184 ++--- src/app/components/Slider.jsx | 772 ++++++++++----------- src/app/components/VerticalBarChart.jsx | 160 ++--- src/app/i18n/de.js | 32 +- src/app/i18n/ko.js | 32 +- src/app/utils/BlueprintFunctions.js | 870 ++++++++++++------------ 7 files changed, 1306 insertions(+), 1306 deletions(-) diff --git a/src/app/components/LineChart.jsx b/src/app/components/LineChart.jsx index b4113ee6..76078aa6 100644 --- a/src/app/components/LineChart.jsx +++ b/src/app/components/LineChart.jsx @@ -1,281 +1,281 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ContainerDimensions from 'react-container-dimensions'; -import * as d3 from 'd3'; -import TranslatedComponent from './TranslatedComponent'; - -const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; - -/** - * Line Chart - */ -export default class LineChart extends TranslatedComponent { - - static defaultProps = { - code: '', - xMin: 0, - yMin: 0, - points: 20, - colors: ['#ff8c0d'], - aspect: 0.5 - }; - - static propTypes = { - func: PropTypes.func.isRequired, - xLabel: PropTypes.string.isRequired, - xMin: PropTypes.number, - xMax: PropTypes.number.isRequired, - xUnit: PropTypes.string.isRequired, - xMark: PropTypes.number, - yLabel: PropTypes.string.isRequired, - yMin: PropTypes.number, - yMax: PropTypes.number.isRequired, - yUnit: PropTypes.string, - series: PropTypes.array, - colors: PropTypes.array, - points: PropTypes.number, - aspect: PropTypes.number, - code: PropTypes.string, - }; - - /** - * 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._updateSeries = this._updateSeries.bind(this); - this._tooltip = this._tooltip.bind(this); - this._showTip = this._showTip.bind(this); - this._hideTip = this._hideTip.bind(this); - this._moveTip = this._moveTip.bind(this); - - const series = props.series; - - let xScale = d3.scaleLinear(); - let yScale = d3.scaleLinear(); - let xAxisScale = d3.scaleLinear(); - - this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); - this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); - - this.state = { - xScale, - xAxisScale, - yScale, - tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), - }; - } - - /** - * Update tooltip content - * @param {number} xPos x coordinate - * @param {number} width current container width - */ - _tooltip(xPos, width) { - let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; - let { xScale, yScale } = this.state; - let { formats, translate } = this.context.language; - let x0 = xScale.invert(xPos), - y0 = func(x0), - tips = this.tipContainer, - yTotal = 0, - flip = (xPos / width > 0.50), - 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 ? ' ' + 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 - * @param {number} width current width of the container - * @returns {Object} calculated dimensions - */ - _updateDimensions(props, scale, width) { - const { xMax, xMin, yMin, yMax } = props; - const innerWidth = width - MARGIN.left - MARGIN.right; - const outerHeight = Math.round(width * props.aspect); - const 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 + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility - return { 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 - * @param {number} width current container width - */ - _moveTip(e, width) { - let clientX = e.touches ? e.touches[0].clientX : e.clientX; - this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); - } - - /** - * Hide tooltip - * @param {SyntheticEvent} e Event - */ - _hideTip(e) { - e.preventDefault(); - this.tipContainer.style('display', 'none'); - this.markersContainer.style('display', 'none'); - } - - /** - * Update series generated from props - * @param {Object} props React Component properties - * @param {Object} state React Component state - */ - _updateSeries(props, state) { - let { func, xMin, xMax, series, points } = props; - let delta = (xMax - xMin) / points; - let seriesData = new Array(points); - - if (delta) { - seriesData = new Array(points); - for (let i = 0, x = xMin; i < points; i++) { - seriesData[i] = [x, func(x)]; - x += delta; - } - seriesData[points - 1] = [xMax, func(xMax)]; - } else { - let yVal = func(xMin); - seriesData = [[0, yVal], [1, yVal]]; - } - - const markerElems = []; - const detailElems = []; - const seriesLines = []; - for (let i = 0, l = series ? series.length : 1; i < l; i++) { - const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); - seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); - detailElems.push(); - markerElems.push(); - } - - const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); - - this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); - } - - /** - * Update dimensions and series data based on props and context. - */ - componentWillMount() { - this._updateSeries(this.props, this.state); - } - - /** - * Update state based on property and context changes - * @param {Object} nextProps Incoming/Next properties - * @param {Object} nextContext Incoming/Next conext - */ - componentWillReceiveProps(nextProps, nextContext) { - const props = this.props; - - if (props.code != nextProps.code) { - this._updateSeries(nextProps, this.state); - } - } - - /** - * Render the chart - * @return {React.Component} Chart SVG - */ - render() { - return ( - - { ({ width, height }) => { - const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height); - const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; - const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; - const lines = seriesLines.map((line, i) => ).reverse(); - - const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; - const xmark = xMark ? : ''; - return ( -
- - - {xmark} - {lines} - d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> - - {xLabel} - ({xUnit}) - - - d3.select(elem).call(this.yAxis)}> - - {yLabel} - { yUnit && ({yUnit}) } - - - this.tipContainer = d3.select(g)} style={{ display: 'none' }}> - - {detailElems} - - this.markersContainer = d3.select(g)} style={{ display: 'none' }}> - {markerElems} - - this._moveTip(e, width)} - onTouchMove={e => this._moveTip(e, width)} - /> - - -
- ); - }} -
- ); - } -} +import React from 'react'; +import PropTypes from 'prop-types'; +import ContainerDimensions from 'react-container-dimensions'; +import * as d3 from 'd3'; +import TranslatedComponent from './TranslatedComponent'; + +const MARGIN = { top: 15, right: 20, bottom: 35, left: 60 }; + +/** + * Line Chart + */ +export default class LineChart extends TranslatedComponent { + + static defaultProps = { + code: '', + xMin: 0, + yMin: 0, + points: 20, + colors: ['#ff8c0d'], + aspect: 0.5 + }; + + static propTypes = { + func: PropTypes.func.isRequired, + xLabel: PropTypes.string.isRequired, + xMin: PropTypes.number, + xMax: PropTypes.number.isRequired, + xUnit: PropTypes.string.isRequired, + xMark: PropTypes.number, + yLabel: PropTypes.string.isRequired, + yMin: PropTypes.number, + yMax: PropTypes.number.isRequired, + yUnit: PropTypes.string, + series: PropTypes.array, + colors: PropTypes.array, + points: PropTypes.number, + aspect: PropTypes.number, + code: PropTypes.string, + }; + + /** + * 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._updateSeries = this._updateSeries.bind(this); + this._tooltip = this._tooltip.bind(this); + this._showTip = this._showTip.bind(this); + this._hideTip = this._hideTip.bind(this); + this._moveTip = this._moveTip.bind(this); + + const series = props.series; + + let xScale = d3.scaleLinear(); + let yScale = d3.scaleLinear(); + let xAxisScale = d3.scaleLinear(); + + this.xAxis = d3.axisBottom(xAxisScale).tickSizeOuter(0); + this.yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); + + this.state = { + xScale, + xAxisScale, + yScale, + tipHeight: 2 + (1.2 * (series ? series.length : 0.8)), + }; + } + + /** + * Update tooltip content + * @param {number} xPos x coordinate + * @param {number} width current container width + */ + _tooltip(xPos, width) { + let { xLabel, yLabel, xUnit, yUnit, func, series } = this.props; + let { xScale, yScale } = this.state; + let { formats, translate } = this.context.language; + let x0 = xScale.invert(xPos), + y0 = func(x0), + tips = this.tipContainer, + yTotal = 0, + flip = (xPos / width > 0.50), + 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 ? ' ' + 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 + * @param {number} width current width of the container + * @returns {Object} calculated dimensions + */ + _updateDimensions(props, scale, width) { + const { xMax, xMin, yMin, yMax } = props; + const innerWidth = width - MARGIN.left - MARGIN.right; + const outerHeight = Math.round(width * props.aspect); + const 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 + (yMax - yMin) * 0.1]); // 10% higher than maximum value for tooltip visibility + return { 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 + * @param {number} width current container width + */ + _moveTip(e, width) { + let clientX = e.touches ? e.touches[0].clientX : e.clientX; + this._tooltip(Math.round(clientX - e.currentTarget.getBoundingClientRect().left), width); + } + + /** + * Hide tooltip + * @param {SyntheticEvent} e Event + */ + _hideTip(e) { + e.preventDefault(); + this.tipContainer.style('display', 'none'); + this.markersContainer.style('display', 'none'); + } + + /** + * Update series generated from props + * @param {Object} props React Component properties + * @param {Object} state React Component state + */ + _updateSeries(props, state) { + let { func, xMin, xMax, series, points } = props; + let delta = (xMax - xMin) / points; + let seriesData = new Array(points); + + if (delta) { + seriesData = new Array(points); + for (let i = 0, x = xMin; i < points; i++) { + seriesData[i] = [x, func(x)]; + x += delta; + } + seriesData[points - 1] = [xMax, func(xMax)]; + } else { + let yVal = func(xMin); + seriesData = [[0, yVal], [1, yVal]]; + } + + const markerElems = []; + const detailElems = []; + const seriesLines = []; + for (let i = 0, l = series ? series.length : 1; i < l; i++) { + const yAccessor = series ? function(d) { return state.yScale(d[1][this]); }.bind(series[i]) : (d) => state.yScale(d[1]); + seriesLines.push(d3.line().x((d, i) => this.state.xScale(d[0])).y(yAccessor)); + detailElems.push(); + markerElems.push(); + } + + const tipHeight = 2 + (1.2 * (seriesLines ? seriesLines.length : 0.8)); + + this.setState({ markerElems, detailElems, seriesLines, seriesData, tipHeight }); + } + + /** + * Update dimensions and series data based on props and context. + */ + componentWillMount() { + this._updateSeries(this.props, this.state); + } + + /** + * Update state based on property and context changes + * @param {Object} nextProps Incoming/Next properties + * @param {Object} nextContext Incoming/Next conext + */ + componentWillReceiveProps(nextProps, nextContext) { + const props = this.props; + + if (props.code != nextProps.code) { + this._updateSeries(nextProps, this.state); + } + } + + /** + * Render the chart + * @return {React.Component} Chart SVG + */ + render() { + return ( + + { ({ width, height }) => { + const { innerWidth, outerHeight, innerHeight } = this._updateDimensions(this.props, this.context.sizeRatio, width, height); + const { xMin, xMax, xLabel, yLabel, xUnit, yUnit, xMark, colors } = this.props; + const { tipHeight, detailElems, markerElems, seriesData, seriesLines } = this.state; + const lines = seriesLines.map((line, i) => ).reverse(); + + const markX = xMark ? innerWidth * (xMark - xMin) / (xMax - xMin) : 0; + const xmark = xMark ? : ''; + return ( +
+ + + {xmark} + {lines} + d3.select(elem).call(this.xAxis)} transform={`translate(0,${innerHeight})`}> + + {xLabel} + ({xUnit}) + + + d3.select(elem).call(this.yAxis)}> + + {yLabel} + { yUnit && ({yUnit}) } + + + this.tipContainer = d3.select(g)} style={{ display: 'none' }}> + + {detailElems} + + this.markersContainer = d3.select(g)} style={{ display: 'none' }}> + {markerElems} + + this._moveTip(e, width)} + onTouchMove={e => this._moveTip(e, width)} + /> + + +
+ ); + }} +
+ ); + } +} diff --git a/src/app/components/PieChart.jsx b/src/app/components/PieChart.jsx index 1c9ccac8..6aa29fd4 100644 --- a/src/app/components/PieChart.jsx +++ b/src/app/components/PieChart.jsx @@ -1,92 +1,92 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import ContainerDimensions from 'react-container-dimensions'; -import * as d3 from 'd3'; - -const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; -const LABEL_COLOUR = '#000000'; - -/** - * A pie chart - */ -export default class PieChart extends Component { - - static propTypes = { - data : PropTypes.array.isRequired - }; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - - this.pie = d3.pie().value((d) => d.value); - this.colors = CORIOLIS_COLOURS; - this.arc = d3.arc(); - this.arc.innerRadius(0); - } - - - /** - * Generate a slice of the pie chart - * @param {Object} d the data for this slice - * @param {number} i the index of this slice - * @param {number} width the current width of the parent container - * @returns {Object} the SVG for the slice - */ - sliceGenerator(d, i, width) { - if (!d || d.value == 0) { - // Ignore 0 values - return null; - } - - const { data } = this.props; - - // Push the labels further out from the centre of the slice - let [labelX, labelY] = this.arc.centroid(d); - const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; - - // Put the keys in a line with equal spacing - const nonZeroItems = data.filter(d => d.value != 0).length; - const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; - const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); - const keyTranslate = `translate(${keyX}, ${width * 0.45})`; - - return ( - - - {d.value} - {d.data.label} - - ); - } - - /** - * Render the component - * @returns {object} Markup - */ - render() { - return ( - - { ({ width }) => { - const pie = this.pie(this.props.data), - translate = `translate(${width / 2}, ${width * 0.4})`; - - this.arc.outerRadius(width * 0.4); - return ( -
- - - {pie.map((d, i) => this.sliceGenerator(d, i, width))} - - -
- ); - }} -
- ); - } -} +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import ContainerDimensions from 'react-container-dimensions'; +import * as d3 from 'd3'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; + +/** + * A pie chart + */ +export default class PieChart extends Component { + + static propTypes = { + data : PropTypes.array.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this.pie = d3.pie().value((d) => d.value); + this.colors = CORIOLIS_COLOURS; + this.arc = d3.arc(); + this.arc.innerRadius(0); + } + + + /** + * Generate a slice of the pie chart + * @param {Object} d the data for this slice + * @param {number} i the index of this slice + * @param {number} width the current width of the parent container + * @returns {Object} the SVG for the slice + */ + sliceGenerator(d, i, width) { + if (!d || d.value == 0) { + // Ignore 0 values + return null; + } + + const { data } = this.props; + + // Push the labels further out from the centre of the slice + let [labelX, labelY] = this.arc.centroid(d); + const labelTranslate = `translate(${labelX * 1.5}, ${labelY * 1.5})`; + + // Put the keys in a line with equal spacing + const nonZeroItems = data.filter(d => d.value != 0).length; + const thisItemIndex = data.slice(0, i + 1).filter(d => d.value != 0).length - 1; + const keyX = -width / 2 + (width / nonZeroItems) * (thisItemIndex + 0.5); + const keyTranslate = `translate(${keyX}, ${width * 0.45})`; + + return ( + + + {d.value} + {d.data.label} + + ); + } + + /** + * Render the component + * @returns {object} Markup + */ + render() { + return ( + + { ({ width }) => { + const pie = this.pie(this.props.data), + translate = `translate(${width / 2}, ${width * 0.4})`; + + this.arc.outerRadius(width * 0.4); + return ( +
+ + + {pie.map((d, i) => this.sliceGenerator(d, i, width))} + + +
+ ); + }} +
+ ); + } +} diff --git a/src/app/components/Slider.jsx b/src/app/components/Slider.jsx index d36ca968..0e5c5696 100644 --- a/src/app/components/Slider.jsx +++ b/src/app/components/Slider.jsx @@ -1,386 +1,386 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const MARGIN_LR = 8; // Left/ Right margin - -/** - * Horizontal Slider - */ -export default class Slider extends React.Component { - - static defaultProps = { - axis: false, - min: 0, - max: 1, - scale: 1 // SVG render scale - }; - - static propTypes = { - axis: PropTypes.bool, - axisUnit: PropTypes.string,// units (T, M, etc.) - max: PropTypes.number, - min: PropTypes.number, - onChange: PropTypes.func.isRequired,// function which determins percent value - onResize: PropTypes.func, - percent: PropTypes.number.isRequired,// value of slider - scale: PropTypes.number - }; - - /** - * Constructor - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - this._down = this._down.bind(this); - this._move = this._move.bind(this); - this._up = this._up.bind(this); - this._keyup = this._keyup.bind(this); - this._keydown = this._keydown.bind(this); - this._touchstart = this._touchstart.bind(this); - this._touchend = this._touchend.bind(this); - - this._updatePercent = this._updatePercent.bind(this); - this._updateDimensions = this._updateDimensions.bind(this); - - this.state = { width: 0 }; - } - - /** - * On Mouse/Touch down handler - * @param {SyntheticEvent} event Event - */ - _down(event) { - let rect = event.currentTarget.getBoundingClientRect(); - this.left = rect.left; - this.width = rect.width; - this._move(event); - this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); - } - - /** - * Update the slider percentage on move - * @param {SyntheticEvent} event Event - */ - _move(event) { - if(this.width !== null && this.left != null) { - let clientX = event.touches ? event.touches[0].clientX : event.clientX; - event.preventDefault(); - this._updatePercent(clientX - this.left, this.width); - } - } - - /** - * On Mouse/Touch up handler - * @param {Event} event DOM Event - */ - _up(event) { - this.sliderInputBox.sliderVal.focus(); - clearTimeout(this.touchStartTimer); - event.preventDefault(); - this.left = null; - this.width = null; - } - - - /** - * Key up handler for keyboard. - * display the number field then set focus to it - * when "Enter" key is pressed - * @param {Event} event Keyboard event - */ - _keyup(event) { - switch (event.key) { - case 'Enter': - event.preventDefault(); - this.sliderInputBox._setDisplay('block'); - return; - default: - return; - } - } - /** - * Key down handler - * increment slider position by +/- 1 when right/left arrow key is pressed or held - * @param {Event} event Keyboard even - */ - _keydown(event) { - let newVal = this.props.percent * this.props.max; - switch (event.key) { - case 'ArrowRight': - newVal += 1; - if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); - return; - case 'ArrowLeft': - newVal -= 1; - if (newVal >= 0) this.props.onChange(newVal / this.props.max); - return; - default: - return; - } - } - - /** - * Touch start handler - * @param {Event} event DOM Event - * - */ - _touchstart(event) { - this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); - } - - /** - * Touch end handler - * @param {Event} event DOM Event - * - */ - _touchend(event) { - this.sliderInputBox.sliderVal.focus(); - clearTimeout(this.touchStartTimer); - } - - /** - * Determine if the user is still dragging - * @param {SyntheticEvent} event Event - */ - _enter(event) { - if(event.buttons !== 1) { - this.left = null; - this.width = null; - } - } - - /** - * Update the slider percentage - * @param {number} pos Slider drag position - * @param {number} width Slider width - * @param {Event} event DOM Event - */ - _updatePercent(pos, width) { - this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); - } - - /** - * Update dimenions from rendered DOM - */ - _updateDimensions() { - this.setState({ - outerWidth: this.node.getBoundingClientRect().width - }); - } - - /** - * Add listeners when about to mount - */ - componentWillMount() { - if (this.props.onResize) { - this.resizeListener = this.props.onResize(this._updateDimensions); - } - } - - /** - * Trigger DOM updates on mount - */ - componentDidMount() { - this._updateDimensions(); - } - - /** - * Remove listeners on unmount - */ - componentWillUnmount() { - if (this.resizeListener) { - this.resizeListener.remove(); - } - } - - /** - * Render the slider - * @return {React.Component} The slider - */ - render() { - let outerWidth = this.state.outerWidth; - let { axis, axisUnit, min, max, scale } = this.props; - let style = { - width: '100%', - height: axis ? '2.5em' : '1.5em', - boxSizing: 'border-box' - }; - if (!outerWidth) { - return this.node = node} />; - } - let margin = MARGIN_LR * scale; - let width = outerWidth - (margin * 2); - let pctPos = width * this.props.percent; - return
this.node = node} tabIndex="0"> - - - - - {axis && - {min + axisUnit} - {(min + max / 2) + axisUnit} - {max + axisUnit} - } - - this.sliderInputBox = tb} - onChange={this.props.onChange} - percent={this.props.percent} - axisUnit={this.props.axisUnit} - scale={this.props.scale} - max={this.props.max} - /> -
; - } -} -/** - * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) - **/ -class TextInputBox extends React.Component { - static propTypes = { - axisUnit: PropTypes.string,// units (T, M, etc.) - max: PropTypes.number, - onChange: PropTypes.func.isRequired,// function which determins percent value - percent: PropTypes.number.isRequired,// value of slider - scale: PropTypes.number - }; - /** - * Determine if the user is still dragging - * @param {Object} props React Component properties - */ - constructor(props) { - super(props); - this._handleFocus = this._handleFocus.bind(this); - this._handleBlur = this._handleBlur.bind(this); - this._handleChange = this._handleChange.bind(this); - this._keyup = this._keyup.bind(this); - this.state = this._getInitialState(); - } - /** - * Update input value if slider changes will change props/state - * @param {Object} nextProps React Component properites - * @param {Object} nextState React Component state values - */ - componentWillReceiveProps(nextProps, nextState) { - let nextValue = nextProps.percent * nextProps.max; - // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form - if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { - this.setState({ inputValue: nextValue }); - } - } - /** - * Update slider textbox visibility/values if changes are made to slider - * @param {Object} prevProps React Component properites - * @param {Object} prevState React Component state values - */ - componentDidUpdate(prevProps, prevState) { - if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { - this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); - } - if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { - // they chose a different module - this.setState({ inputValue: this.props.max }); - } - if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { - this.props.onChange(this.state.inputValue / this.props.max); - } - } - /** - * Set initial state for the textbox. - * We may want to rethink this to - * try and make it a stateless component - * @returns {object} React state object with initial values set - */ - _getInitialState() { - return { - divStyle: { display:'none' }, - inputStyle: { width:'4em' }, - labelStyle: { marginLeft: '.1em' }, - maxLength:5, - size:5, - min:0, - tabIndex:-1, - type:'number', - readOnly: true, - inputValue: this.props.percent * this.props.max - }; - } - /** - * - * @param {string} val block or none - */ - _setDisplay(val) { - this.setState({ - divStyle: { display:val } - }); - } - /** - * Update the input value - * when textbox gets focus - */ - _handleFocus() { - this.setState({ - inputValue:this._getValue() - }); - } - /** - * Update inputValue when textbox loses focus - */ - _handleBlur() { - this._setDisplay('none'); - if (this.state.inputValue !== '') { - this.props.onChange(this.state.inputValue / this.props.max); - } else { - this.setState({ - inputValue: this.props.percent * this.props.max - }); - } - } - /** - * Get the value in the text box - * @returns {number} inputValue Value of the input box - */ - _getValue() { - return this.state.inputValue; - } - /** - * Update and set limits on input box - * values depending on what user - * has selected - * - * @param {SyntheticEvent} event ReactJs onChange event - */ - _handleChange(event) { - if (event.target.value < 0) { - this.setState({ inputValue: 0 }); - } else if (event.target.value <= this.props.max) { - this.setState({ inputValue: event.target.value }); - } else { - this.setState({ inputValue: this.props.max }); - } - } - /** - * Key up handler for input field. - * If user hits Enter key, blur/close the input field - * @param {Event} event Keyboard event - */ - _keyup(event) { - switch (event.key) { - case 'Enter': - this.sliderVal.blur(); - return; - default: - return; - } - } - /** - * Get the value in the text box - * @return {React.Component} Text Input component for Slider - */ - render() { - let { axisUnit, onChange, percent, scale } = this.props; - return
{this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/>{this.props.axisUnit}
; - } -} - +import React from 'react'; +import PropTypes from 'prop-types'; + +const MARGIN_LR = 8; // Left/ Right margin + +/** + * Horizontal Slider + */ +export default class Slider extends React.Component { + + static defaultProps = { + axis: false, + min: 0, + max: 1, + scale: 1 // SVG render scale + }; + + static propTypes = { + axis: PropTypes.bool, + axisUnit: PropTypes.string,// units (T, M, etc.) + max: PropTypes.number, + min: PropTypes.number, + onChange: PropTypes.func.isRequired,// function which determins percent value + onResize: PropTypes.func, + percent: PropTypes.number.isRequired,// value of slider + scale: PropTypes.number + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._down = this._down.bind(this); + this._move = this._move.bind(this); + this._up = this._up.bind(this); + this._keyup = this._keyup.bind(this); + this._keydown = this._keydown.bind(this); + this._touchstart = this._touchstart.bind(this); + this._touchend = this._touchend.bind(this); + + this._updatePercent = this._updatePercent.bind(this); + this._updateDimensions = this._updateDimensions.bind(this); + + this.state = { width: 0 }; + } + + /** + * On Mouse/Touch down handler + * @param {SyntheticEvent} event Event + */ + _down(event) { + let rect = event.currentTarget.getBoundingClientRect(); + this.left = rect.left; + this.width = rect.width; + this._move(event); + this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); + } + + /** + * Update the slider percentage on move + * @param {SyntheticEvent} event Event + */ + _move(event) { + if(this.width !== null && this.left != null) { + let clientX = event.touches ? event.touches[0].clientX : event.clientX; + event.preventDefault(); + this._updatePercent(clientX - this.left, this.width); + } + } + + /** + * On Mouse/Touch up handler + * @param {Event} event DOM Event + */ + _up(event) { + this.sliderInputBox.sliderVal.focus(); + clearTimeout(this.touchStartTimer); + event.preventDefault(); + this.left = null; + this.width = null; + } + + + /** + * Key up handler for keyboard. + * display the number field then set focus to it + * when "Enter" key is pressed + * @param {Event} event Keyboard event + */ + _keyup(event) { + switch (event.key) { + case 'Enter': + event.preventDefault(); + this.sliderInputBox._setDisplay('block'); + return; + default: + return; + } + } + /** + * Key down handler + * increment slider position by +/- 1 when right/left arrow key is pressed or held + * @param {Event} event Keyboard even + */ + _keydown(event) { + let newVal = this.props.percent * this.props.max; + switch (event.key) { + case 'ArrowRight': + newVal += 1; + if (newVal <= this.props.max) this.props.onChange(newVal / this.props.max); + return; + case 'ArrowLeft': + newVal -= 1; + if (newVal >= 0) this.props.onChange(newVal / this.props.max); + return; + default: + return; + } + } + + /** + * Touch start handler + * @param {Event} event DOM Event + * + */ + _touchstart(event) { + this.touchStartTimer = setTimeout(() => this.sliderInputBox._setDisplay('block'), 1500); + } + + /** + * Touch end handler + * @param {Event} event DOM Event + * + */ + _touchend(event) { + this.sliderInputBox.sliderVal.focus(); + clearTimeout(this.touchStartTimer); + } + + /** + * Determine if the user is still dragging + * @param {SyntheticEvent} event Event + */ + _enter(event) { + if(event.buttons !== 1) { + this.left = null; + this.width = null; + } + } + + /** + * Update the slider percentage + * @param {number} pos Slider drag position + * @param {number} width Slider width + * @param {Event} event DOM Event + */ + _updatePercent(pos, width) { + this.props.onChange(Math.min(Math.max(pos / width, 0), 1)); + } + + /** + * Update dimenions from rendered DOM + */ + _updateDimensions() { + this.setState({ + outerWidth: this.node.getBoundingClientRect().width + }); + } + + /** + * Add listeners when about to mount + */ + componentWillMount() { + if (this.props.onResize) { + this.resizeListener = this.props.onResize(this._updateDimensions); + } + } + + /** + * Trigger DOM updates on mount + */ + componentDidMount() { + this._updateDimensions(); + } + + /** + * Remove listeners on unmount + */ + componentWillUnmount() { + if (this.resizeListener) { + this.resizeListener.remove(); + } + } + + /** + * Render the slider + * @return {React.Component} The slider + */ + render() { + let outerWidth = this.state.outerWidth; + let { axis, axisUnit, min, max, scale } = this.props; + let style = { + width: '100%', + height: axis ? '2.5em' : '1.5em', + boxSizing: 'border-box' + }; + if (!outerWidth) { + return this.node = node} />; + } + let margin = MARGIN_LR * scale; + let width = outerWidth - (margin * 2); + let pctPos = width * this.props.percent; + return
this.node = node} tabIndex="0"> + + + + + {axis && + {min + axisUnit} + {(min + max / 2) + axisUnit} + {max + axisUnit} + } + + this.sliderInputBox = tb} + onChange={this.props.onChange} + percent={this.props.percent} + axisUnit={this.props.axisUnit} + scale={this.props.scale} + max={this.props.max} + /> +
; + } +} +/** + * New component to add keyboard support for sliders - works on all devices (desktop, iOS, Android) + **/ +class TextInputBox extends React.Component { + static propTypes = { + axisUnit: PropTypes.string,// units (T, M, etc.) + max: PropTypes.number, + onChange: PropTypes.func.isRequired,// function which determins percent value + percent: PropTypes.number.isRequired,// value of slider + scale: PropTypes.number + }; + /** + * Determine if the user is still dragging + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + this._handleFocus = this._handleFocus.bind(this); + this._handleBlur = this._handleBlur.bind(this); + this._handleChange = this._handleChange.bind(this); + this._keyup = this._keyup.bind(this); + this.state = this._getInitialState(); + } + /** + * Update input value if slider changes will change props/state + * @param {Object} nextProps React Component properites + * @param {Object} nextState React Component state values + */ + componentWillReceiveProps(nextProps, nextState) { + let nextValue = nextProps.percent * nextProps.max; + // See https://stackoverflow.com/questions/32414308/updating-state-on-props-change-in-react-form + if (nextValue !== this.state.inputValue && nextValue <= nextProps.max) { + this.setState({ inputValue: nextValue }); + } + } + /** + * Update slider textbox visibility/values if changes are made to slider + * @param {Object} prevProps React Component properites + * @param {Object} prevState React Component state values + */ + componentDidUpdate(prevProps, prevState) { + if (prevState.divStyle.display == 'none' && this.state.divStyle.display == 'block') { + this.enterTimer = setTimeout(() => this.sliderVal.focus(), 10); + } + if (prevProps.max !== this.props.max && this.state.inputValue > this.props.max) { + // they chose a different module + this.setState({ inputValue: this.props.max }); + } + if (this.state.inputValue != prevState.inputValue && prevProps.max == this.props.max) { + this.props.onChange(this.state.inputValue / this.props.max); + } + } + /** + * Set initial state for the textbox. + * We may want to rethink this to + * try and make it a stateless component + * @returns {object} React state object with initial values set + */ + _getInitialState() { + return { + divStyle: { display:'none' }, + inputStyle: { width:'4em' }, + labelStyle: { marginLeft: '.1em' }, + maxLength:5, + size:5, + min:0, + tabIndex:-1, + type:'number', + readOnly: true, + inputValue: this.props.percent * this.props.max + }; + } + /** + * + * @param {string} val block or none + */ + _setDisplay(val) { + this.setState({ + divStyle: { display:val } + }); + } + /** + * Update the input value + * when textbox gets focus + */ + _handleFocus() { + this.setState({ + inputValue:this._getValue() + }); + } + /** + * Update inputValue when textbox loses focus + */ + _handleBlur() { + this._setDisplay('none'); + if (this.state.inputValue !== '') { + this.props.onChange(this.state.inputValue / this.props.max); + } else { + this.setState({ + inputValue: this.props.percent * this.props.max + }); + } + } + /** + * Get the value in the text box + * @returns {number} inputValue Value of the input box + */ + _getValue() { + return this.state.inputValue; + } + /** + * Update and set limits on input box + * values depending on what user + * has selected + * + * @param {SyntheticEvent} event ReactJs onChange event + */ + _handleChange(event) { + if (event.target.value < 0) { + this.setState({ inputValue: 0 }); + } else if (event.target.value <= this.props.max) { + this.setState({ inputValue: event.target.value }); + } else { + this.setState({ inputValue: this.props.max }); + } + } + /** + * Key up handler for input field. + * If user hits Enter key, blur/close the input field + * @param {Event} event Keyboard event + */ + _keyup(event) { + switch (event.key) { + case 'Enter': + this.sliderVal.blur(); + return; + default: + return; + } + } + /** + * Get the value in the text box + * @return {React.Component} Text Input component for Slider + */ + render() { + let { axisUnit, onChange, percent, scale } = this.props; + return
{this._handleBlur();}} onFocus={() => {this._handleFocus();}} type={this.state.type} ref={(ip) => this.sliderVal = ip}/>{this.props.axisUnit}
; + } +} + diff --git a/src/app/components/VerticalBarChart.jsx b/src/app/components/VerticalBarChart.jsx index ad77233c..74011e0a 100644 --- a/src/app/components/VerticalBarChart.jsx +++ b/src/app/components/VerticalBarChart.jsx @@ -1,80 +1,80 @@ -import TranslatedComponent from './TranslatedComponent'; -import React, { PropTypes } from 'react'; -import ContainerDimensions from 'react-container-dimensions'; -import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts'; - -const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; -const LABEL_COLOUR = '#000000'; -const AXIS_COLOUR = '#C06400'; - -const ASPECT = 1; - -const merge = function(one, two) { - return Object.assign({}, one, two); -}; - -/** - * A vertical bar chart - */ -export default class VerticalBarChart extends TranslatedComponent { - static propTypes = { - data : PropTypes.array.isRequired, - yMax : PropTypes.number - }; - - /** - * Constructor - * @param {Object} props React Component properties - * @param {Object} context React Component context - */ - constructor(props, context) { - super(props); - - this._termtip = this._termtip.bind(this); - } - - /** - * Render the bar chart - * @returns {Object} the markup - */ - render() { - const { tooltip, termtip } = this.context; - - // Calculate maximum for Y - let dataMax = Math.max(...this.props.data.map(d => d.value)); - if (dataMax == -Infinity) dataMax = 0; - let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; - const localMax = Math.max(dataMax, yMax); - - return ( - - { ({ width }) => ( -
- - - - - - - -
- )} -
- ); - } - - /** - * Generate a term tip - * @param {Object} d the data - * @param {number} i the index - * @param {Object} e the event - * @returns {Object} termtip markup - */ - _termtip(d, i, e) { - if (this.props.data[i].tooltip) { - return this.context.termtip(this.props.data[i].tooltip, e); - } else { - return null; - } - } -} +import TranslatedComponent from './TranslatedComponent'; +import React, { PropTypes } from 'react'; +import ContainerDimensions from 'react-container-dimensions'; +import { BarChart, Bar, XAxis, YAxis, LabelList } from 'recharts'; + +const CORIOLIS_COLOURS = ['#FF8C0D', '#1FB0FF', '#71A052', '#D5D54D']; +const LABEL_COLOUR = '#000000'; +const AXIS_COLOUR = '#C06400'; + +const ASPECT = 1; + +const merge = function(one, two) { + return Object.assign({}, one, two); +}; + +/** + * A vertical bar chart + */ +export default class VerticalBarChart extends TranslatedComponent { + static propTypes = { + data : PropTypes.array.isRequired, + yMax : PropTypes.number + }; + + /** + * Constructor + * @param {Object} props React Component properties + * @param {Object} context React Component context + */ + constructor(props, context) { + super(props); + + this._termtip = this._termtip.bind(this); + } + + /** + * Render the bar chart + * @returns {Object} the markup + */ + render() { + const { tooltip, termtip } = this.context; + + // Calculate maximum for Y + let dataMax = Math.max(...this.props.data.map(d => d.value)); + if (dataMax == -Infinity) dataMax = 0; + let yMax = this.props.yMax ? Math.round(this.props.yMax) : 0; + const localMax = Math.max(dataMax, yMax); + + return ( + + { ({ width }) => ( +
+ + + + + + + +
+ )} +
+ ); + } + + /** + * Generate a term tip + * @param {Object} d the data + * @param {number} i the index + * @param {Object} e the event + * @returns {Object} termtip markup + */ + _termtip(d, i, e) { + if (this.props.data[i].tooltip) { + return this.context.termtip(this.props.data[i].tooltip, e); + } else { + return null; + } + } +} diff --git a/src/app/i18n/de.js b/src/app/i18n/de.js index b1ec1c33..5261a9cb 100644 --- a/src/app/i18n/de.js +++ b/src/app/i18n/de.js @@ -1,16 +1,16 @@ -export const formats = { - 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'] -}; - -export { default as terms } from './de.json'; \ No newline at end of file +export const formats = { + 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'] +}; + +export { default as terms } from './de.json'; diff --git a/src/app/i18n/ko.js b/src/app/i18n/ko.js index c2a2887e..ded3d7d1 100644 --- a/src/app/i18n/ko.js +++ b/src/app/i18n/ko.js @@ -1,16 +1,16 @@ -export const formats = { - decimal: '.', - thousands: ',', - grouping: [3], - currency: ['₩', ''], - dateTime: '%a %b %e %X %Y', - date: '%Y/%m/%d', - time: '%H:%M:%S', - periods: ['오전', '오후'], - days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], - shortDays: ['일', '월', '화', '수', '목', '금', '토'], - months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], - shortMonths: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'] -}; - -export { default as terms } from './ko.json'; +export const formats = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['₩', ''], + dateTime: '%a %b %e %X %Y', + date: '%Y/%m/%d', + time: '%H:%M:%S', + periods: ['오전', '오후'], + days: ['일요일', '월요일', '화요일', '수요일', '목요일', '금요일', '토요일'], + shortDays: ['일', '월', '화', '수', '목', '금', '토'], + months: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'], + shortMonths: ['1월', '2월', '3월', '4월', '5월', '6월', '7월', '8월', '9월', '10월', '11월', '12월'] +}; + +export { default as terms } from './ko.json'; diff --git a/src/app/utils/BlueprintFunctions.js b/src/app/utils/BlueprintFunctions.js index 80259bea..3365c05c 100644 --- a/src/app/utils/BlueprintFunctions.js +++ b/src/app/utils/BlueprintFunctions.js @@ -1,435 +1,435 @@ -import React from 'react'; -import { Modifications } from 'coriolis-data/dist'; -import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; - -/** - * Generate a tooltip with details of a blueprint's specials - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @param {string} specialName The name of the special - * @returns {Object} The react components - */ -export function specialToolTip(translate, blueprint, grp, m, specialName) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - if (m) { - // We also add in any benefits from specials that aren't covered above - if (m.blueprint) { - for (const feature in Modifications.modifierActions[specialName]) { - // if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature) - m.getModValue(feature, true); - if (featureDef.type === 'percentage') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - - return ( -
- - - {effects} - -
-
- ); -} - -/** - * Generate a tooltip with details of a blueprint's effects - * @param {Object} translate The translate object - * @param {Object} blueprint The blueprint at the required grade - * @param {Array} engineers The engineers supplying this blueprint - * @param {string} grp The group of the module - * @param {Object} m The module to compare with - * @returns {Object} The react components - */ -export function blueprintTooltip(translate, blueprint, engineers, grp, m) { - const effects = []; - if (!blueprint || !blueprint.features) { - return undefined; - } - for (const feature in blueprint.features) { - const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); - const featureDef = Modifications.modifications[feature]; - if (!featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let lowerBound = blueprint.features[feature][0]; - let upperBound = blueprint.features[feature][1]; - if (featureDef.type === 'percentage') { - lowerBound = Math.round(lowerBound * 1000) / 10; - upperBound = Math.round(upperBound * 1000) / 10; - } - const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); - const upperIsBeneficial = isValueBeneficial(feature, upperBound); - if (m) { - // We have a module - add in the current value - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {current}{symbol} - {upperBound}{symbol} - - ); - } else { - // We do not have a module, no value - effects.push( - - {translate(feature, grp)} - {lowerBound}{symbol} - {upperBound}{symbol} - - ); - } - } - } - if (m) { - // Because we have a module add in any benefits that aren't part of the primary blueprint - for (const feature in m.mods) { - if (!blueprint.features[feature]) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - - // We also add in any benefits from specials that aren't covered above - if (m.blueprint && m.blueprint.special) { - for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { - if (!blueprint.features[feature] && !m.mods.feature) { - const featureDef = Modifications.modifications[feature]; - if (featureDef && !featureDef.hidden) { - let symbol = ''; - if (feature === 'jitter') { - symbol = '°'; - } else if (featureDef.type === 'percentage') { - symbol = '%'; - } - let current = m.getModValue(feature); - if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { - current = Math.round(current / 10) / 10; - } else if (featureDef.type === 'numeric') { - current /= 100; - } - const currentIsBeneficial = isValueBeneficial(feature, current); - effects.push( - - {translate(feature, grp)} -   - {current}{symbol} -   - - ); - } - } - } - } - } - - let components; - if (!m) { - components = []; - for (const component in blueprint.components) { - components.push( - - {translate(component)} - {blueprint.components[component]} - - ); - } - } - - let engineersList; - if (engineers) { - engineersList = []; - for (const engineer of engineers) { - engineersList.push( - - {engineer} - - ); - } - } - - return ( -
- - - - - - {m ? : null } - - - - - {effects} - -
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
- { components ? - - - - - - - - {components} - -
{translate('component')}{translate('amount')}
: null } - { engineersList ? - - - - - - - {engineersList} - -
{translate('engineers')}
: null } -
- ); -} - -/** - * Is this blueprint feature beneficial? - * @param {string} feature The name of the feature - * @param {array} values The value of the feature - * @returns {boolean} True if this feature is beneficial - */ -export function isBeneficial(feature, values) { - const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); - if (Modifications.modifications[feature].higherbetter) { - return !fact; - } else { - return fact; - } -} - -/** - * Is this feature value beneficial? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature - * @returns {boolean} True if this value is beneficial - */ -export function isValueBeneficial(feature, value) { - if (Modifications.modifications[feature].higherbetter) { - return value > 0; - } else { - return value < 0; - } -} - -/** - * Is the change as shown beneficial? - * @param {string} feature The name of the feature - * @param {number} value The value of the feature as percentage change - * @returns True if the value is beneficial - */ -export function isChangeValueBeneficial(feature, value) { - let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; - if (changeHigherBetter === undefined) { - return isValueBeneficial(feature, value); - } - - if (changeHigherBetter) { - return value > 0; - } else { - return value < 0; - } -} - -/** - * Get a blueprint with a given name and an optional module - * @param {string} name The name of the blueprint - * @param {Object} module The module for which to obtain this blueprint - * @returns {Object} The matching blueprint - */ -export function getBlueprint(name, module) { - // Start with a copy of the blueprint - const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); - const found = Modifications.blueprints[findMod(name)]; - if (!found || !found.fdname) { - return {}; - } - const blueprint = JSON.parse(JSON.stringify(found)); - return blueprint; -} - -/** - * Provide 'percent' primary modifications - * @param {Object} ship The ship for which to perform the modifications - * @param {Object} m The module for which to perform the modifications - * @param {Number} percent The percent to set values to of full. - */ -export function setPercent(ship, m, percent) { - ship.clearModifications(m); - // Pick given value as multiplier - const mult = percent / 100; - setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); -} - -/** - * Sets the blueprint quality and fires a callback for each property affected. - * @param {Object} blueprint The ship for which to perform the modifications - * @param {Number} quality The quality to apply - float number 0 to 1. - * @param {Function} cb The Callback to run for each property. Function (featureName, value) - */ -export function setQualityCB(blueprint, quality, cb) { - // Pick given value as multiplier - const features = blueprint.grades[blueprint.grade].features; - for (const featureName in features) { - let value; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } else { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } - } else { - // Higher is worse, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); - } else { - value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); - } - } - - if (Modifications.modifications[featureName].type == 'percentage') { - value = value * 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - value = value * 100; - } - - cb(featureName, value); - } -} - -/** - * Provide 'random' primary modifications - * @param {Object} ship The ship for which to perform the modifications - * @param {Object} m The module for which to perform the modifications - */ -export function setRandom(ship, m) { - // Pick a single value for our randomness - setPercent(ship, m, Math.random() * 100); -} - -/** - * Provide 'percent' primary query - * @param {Object} m The module for which to perform the query - * @returns {Number} percent The percentage indicator of current applied values. - */ -export function getPercent(m) { - let result = null; - const features = m.blueprint.grades[m.blueprint.grade].features; - for (const featureName in features) { - if (features[featureName][0] === features[featureName][1]) { - continue; - } - - let value = _getValue(m, featureName); - let mult; - if (Modifications.modifications[featureName].higherbetter) { - // Higher is better, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); - } else { - mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); - } - } else { - // Higher is worse, but is this making it better or worse? - if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { - mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); - } else { - mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); - } - } - - if (result && result != mult) { - return null; - } else if (result != mult) { - result = mult; - } - } - - return result; -} - -/** - * Query a feature value - * @param {Object} m The module for which to perform the query - * @param {string} featureName The feature being queried - * @returns {number} The value of the modification as a % - */ -function _getValue(m, featureName) { - if (Modifications.modifications[featureName].type == 'percentage') { - return m.getModValue(featureName, true) / 10000; - } else if (Modifications.modifications[featureName].type == 'numeric') { - return m.getModValue(featureName, true) / 100; - } else { - return m.getModValue(featureName, true); - } -} +import React from 'react'; +import { Modifications } from 'coriolis-data/dist'; +import { STATS_FORMATTING } from '../shipyard/StatsFormatting'; + +/** + * Generate a tooltip with details of a blueprint's specials + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @param {string} specialName The name of the special + * @returns {Object} The react components + */ +export function specialToolTip(translate, blueprint, grp, m, specialName) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + if (m) { + // We also add in any benefits from specials that aren't covered above + if (m.blueprint) { + for (const feature in Modifications.modifierActions[specialName]) { + // if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature) - m.getModValue(feature, true); + if (featureDef.type === 'percentage') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + + return ( +
+ + + {effects} + +
+
+ ); +} + +/** + * Generate a tooltip with details of a blueprint's effects + * @param {Object} translate The translate object + * @param {Object} blueprint The blueprint at the required grade + * @param {Array} engineers The engineers supplying this blueprint + * @param {string} grp The group of the module + * @param {Object} m The module to compare with + * @returns {Object} The react components + */ +export function blueprintTooltip(translate, blueprint, engineers, grp, m) { + const effects = []; + if (!blueprint || !blueprint.features) { + return undefined; + } + for (const feature in blueprint.features) { + const featureIsBeneficial = isBeneficial(feature, blueprint.features[feature]); + const featureDef = Modifications.modifications[feature]; + if (!featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let lowerBound = blueprint.features[feature][0]; + let upperBound = blueprint.features[feature][1]; + if (featureDef.type === 'percentage') { + lowerBound = Math.round(lowerBound * 1000) / 10; + upperBound = Math.round(upperBound * 1000) / 10; + } + const lowerIsBeneficial = isValueBeneficial(feature, lowerBound); + const upperIsBeneficial = isValueBeneficial(feature, upperBound); + if (m) { + // We have a module - add in the current value + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {current}{symbol} + {upperBound}{symbol} + + ); + } else { + // We do not have a module, no value + effects.push( + + {translate(feature, grp)} + {lowerBound}{symbol} + {upperBound}{symbol} + + ); + } + } + } + if (m) { + // Because we have a module add in any benefits that aren't part of the primary blueprint + for (const feature in m.mods) { + if (!blueprint.features[feature]) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + + // We also add in any benefits from specials that aren't covered above + if (m.blueprint && m.blueprint.special) { + for (const feature in Modifications.modifierActions[m.blueprint.special.edname]) { + if (!blueprint.features[feature] && !m.mods.feature) { + const featureDef = Modifications.modifications[feature]; + if (featureDef && !featureDef.hidden) { + let symbol = ''; + if (feature === 'jitter') { + symbol = '°'; + } else if (featureDef.type === 'percentage') { + symbol = '%'; + } + let current = m.getModValue(feature); + if (featureDef.type === 'percentage' || featureDef.name === 'burst' || featureDef.name === 'burstrof') { + current = Math.round(current / 10) / 10; + } else if (featureDef.type === 'numeric') { + current /= 100; + } + const currentIsBeneficial = isValueBeneficial(feature, current); + effects.push( + + {translate(feature, grp)} +   + {current}{symbol} +   + + ); + } + } + } + } + } + + let components; + if (!m) { + components = []; + for (const component in blueprint.components) { + components.push( + + {translate(component)} + {blueprint.components[component]} + + ); + } + } + + let engineersList; + if (engineers) { + engineersList = []; + for (const engineer of engineers) { + engineersList.push( + + {engineer} + + ); + } + } + + return ( +
+ + + + + + {m ? : null } + + + + + {effects} + +
{translate('feature')}{translate('worst')}{translate('current')}{translate('best')}
+ { components ? + + + + + + + + {components} + +
{translate('component')}{translate('amount')}
: null } + { engineersList ? + + + + + + + {engineersList} + +
{translate('engineers')}
: null } +
+ ); +} + +/** + * Is this blueprint feature beneficial? + * @param {string} feature The name of the feature + * @param {array} values The value of the feature + * @returns {boolean} True if this feature is beneficial + */ +export function isBeneficial(feature, values) { + const fact = (values[0] < 0 || (values[0] === 0 && values[1] < 0)); + if (Modifications.modifications[feature].higherbetter) { + return !fact; + } else { + return fact; + } +} + +/** + * Is this feature value beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature + * @returns {boolean} True if this value is beneficial + */ +export function isValueBeneficial(feature, value) { + if (Modifications.modifications[feature].higherbetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Is the change as shown beneficial? + * @param {string} feature The name of the feature + * @param {number} value The value of the feature as percentage change + * @returns True if the value is beneficial + */ +export function isChangeValueBeneficial(feature, value) { + let changeHigherBetter = STATS_FORMATTING[feature].higherbetter; + if (changeHigherBetter === undefined) { + return isValueBeneficial(feature, value); + } + + if (changeHigherBetter) { + return value > 0; + } else { + return value < 0; + } +} + +/** + * Get a blueprint with a given name and an optional module + * @param {string} name The name of the blueprint + * @param {Object} module The module for which to obtain this blueprint + * @returns {Object} The matching blueprint + */ +export function getBlueprint(name, module) { + // Start with a copy of the blueprint + const findMod = val => Object.keys(Modifications.blueprints).find(elem => elem.toString().toLowerCase().search(val.toString().toLowerCase().replace(/(OutfittingFieldType_|persecond)/igm, '')) >= 0); + const found = Modifications.blueprints[findMod(name)]; + if (!found || !found.fdname) { + return {}; + } + const blueprint = JSON.parse(JSON.stringify(found)); + return blueprint; +} + +/** + * Provide 'percent' primary modifications + * @param {Object} ship The ship for which to perform the modifications + * @param {Object} m The module for which to perform the modifications + * @param {Number} percent The percent to set values to of full. + */ +export function setPercent(ship, m, percent) { + ship.clearModifications(m); + // Pick given value as multiplier + const mult = percent / 100; + setQualityCB(m.blueprint, mult, (featureName, value) => ship.setModification(m, featureName, value)); +} + +/** + * Sets the blueprint quality and fires a callback for each property affected. + * @param {Object} blueprint The ship for which to perform the modifications + * @param {Number} quality The quality to apply - float number 0 to 1. + * @param {Function} cb The Callback to run for each property. Function (featureName, value) + */ +export function setQualityCB(blueprint, quality, cb) { + // Pick given value as multiplier + const features = blueprint.grades[blueprint.grade].features; + for (const featureName in features) { + let value; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } else { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } + } else { + // Higher is worse, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + value = features[featureName][0] + ((features[featureName][1] - features[featureName][0]) * quality); + } else { + value = features[featureName][1] + ((features[featureName][0] - features[featureName][1]) * quality); + } + } + + if (Modifications.modifications[featureName].type == 'percentage') { + value = value * 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + value = value * 100; + } + + cb(featureName, value); + } +} + +/** + * Provide 'random' primary modifications + * @param {Object} ship The ship for which to perform the modifications + * @param {Object} m The module for which to perform the modifications + */ +export function setRandom(ship, m) { + // Pick a single value for our randomness + setPercent(ship, m, Math.random() * 100); +} + +/** + * Provide 'percent' primary query + * @param {Object} m The module for which to perform the query + * @returns {Number} percent The percentage indicator of current applied values. + */ +export function getPercent(m) { + let result = null; + const features = m.blueprint.grades[m.blueprint.grade].features; + for (const featureName in features) { + if (features[featureName][0] === features[featureName][1]) { + continue; + } + + let value = _getValue(m, featureName); + let mult; + if (Modifications.modifications[featureName].higherbetter) { + // Higher is better, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); + } else { + mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); + } + } else { + // Higher is worse, but is this making it better or worse? + if (features[featureName][0] < 0 || (features[featureName][0] === 0 && features[featureName][1] < 0)) { + mult = Math.round((value - features[featureName][0]) / (features[featureName][1] - features[featureName][0]) * 100); + } else { + mult = Math.round((value - features[featureName][1]) / (features[featureName][0] - features[featureName][1]) * 100); + } + } + + if (result && result != mult) { + return null; + } else if (result != mult) { + result = mult; + } + } + + return result; +} + +/** + * Query a feature value + * @param {Object} m The module for which to perform the query + * @param {string} featureName The feature being queried + * @returns {number} The value of the modification as a % + */ +function _getValue(m, featureName) { + if (Modifications.modifications[featureName].type == 'percentage') { + return m.getModValue(featureName, true) / 10000; + } else if (Modifications.modifications[featureName].type == 'numeric') { + return m.getModValue(featureName, true) / 100; + } else { + return m.getModValue(featureName, true); + } +} From cd68199a4163f7f283b1ae4c6d101e04a707d904 Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 17 May 2024 12:31:32 +0100 Subject: [PATCH 4/8] Adding workflow for autodeploy --- .github/workflows/autodeploy.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/autodeploy.yml diff --git a/.github/workflows/autodeploy.yml b/.github/workflows/autodeploy.yml new file mode 100644 index 00000000..f150f835 --- /dev/null +++ b/.github/workflows/autodeploy.yml @@ -0,0 +1,29 @@ +# This is a basic deployment workflow triggered by pushes to the alpha branch. + +name: Auto-Deploy + +# Controls when the action will run. Workflow runs when the alpha branch receives a push event +on: + workflow_dispatch: + push: + branches: + - alpha + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + downloadcode: + runs-on: self-hosted + steps: + - shell: bash + run: | + git clone https://github.com/alex-williams/coriolis-data.git + cd coriolis-data + git checkout alpha + npm install + cd ../ + git clone https://github.com/alex-williams/coriolis.git + cd ./coriolis + git checkout alpha + npm install + npm run build + sudo -u www-data cp -r ./build/* /var/www/newdisk/coriolis.brighter-applications.co.uk/ \ No newline at end of file From fbd9c3d282bd1d242f23667a347657e52117a95b Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 17 May 2024 12:44:07 +0100 Subject: [PATCH 5/8] Improving workflow --- .github/workflows/autodeploy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/autodeploy.yml b/.github/workflows/autodeploy.yml index f150f835..4cc9fbe7 100644 --- a/.github/workflows/autodeploy.yml +++ b/.github/workflows/autodeploy.yml @@ -16,11 +16,13 @@ jobs: steps: - shell: bash run: | + rm -Rf ./coriolis-data git clone https://github.com/alex-williams/coriolis-data.git cd coriolis-data git checkout alpha npm install cd ../ + rm -Rf ./coriolis git clone https://github.com/alex-williams/coriolis.git cd ./coriolis git checkout alpha From 4283b0b839fc3d6bd434460152eabbb93095096f Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 24 May 2024 17:38:47 +0100 Subject: [PATCH 6/8] Changed deployment ordering --- .github/workflows/autodeploy.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autodeploy.yml b/.github/workflows/autodeploy.yml index 4cc9fbe7..9ef7e497 100644 --- a/.github/workflows/autodeploy.yml +++ b/.github/workflows/autodeploy.yml @@ -16,15 +16,14 @@ jobs: steps: - shell: bash run: | + rm -Rf ./coriolis rm -Rf ./coriolis-data + git clone https://github.com/alex-williams/coriolis.git git clone https://github.com/alex-williams/coriolis-data.git cd coriolis-data git checkout alpha npm install - cd ../ - rm -Rf ./coriolis - git clone https://github.com/alex-williams/coriolis.git - cd ./coriolis + cd ../coriolis git checkout alpha npm install npm run build From 0d749202e27041799c3b024652933d16ffe92eff Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 24 May 2024 17:41:27 +0100 Subject: [PATCH 7/8] Changing to clone single branch for deployment, not the whole repo --- .github/workflows/autodeploy.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/autodeploy.yml b/.github/workflows/autodeploy.yml index 9ef7e497..8f28cc84 100644 --- a/.github/workflows/autodeploy.yml +++ b/.github/workflows/autodeploy.yml @@ -18,13 +18,11 @@ jobs: run: | rm -Rf ./coriolis rm -Rf ./coriolis-data - git clone https://github.com/alex-williams/coriolis.git - git clone https://github.com/alex-williams/coriolis-data.git + git clone https://github.com/alex-williams/coriolis.git --single-branch --branch alpha + git clone https://github.com/alex-williams/coriolis-data.git --single-branch --branch alpha cd coriolis-data - git checkout alpha npm install cd ../coriolis - git checkout alpha npm install npm run build sudo -u www-data cp -r ./build/* /var/www/newdisk/coriolis.brighter-applications.co.uk/ \ No newline at end of file From ee92f2f2e4e98a617c8be93b1be43c728f9a7c6f Mon Sep 17 00:00:00 2001 From: Alex Williams Date: Fri, 24 May 2024 17:56:20 +0100 Subject: [PATCH 8/8] Adds the Advanced MC's, AX MC's, AX MR's and Nanite Torpedo (#4) --- src/app/components/AvailableModulesMenu.jsx | 8 ++++++-- src/app/i18n/en.json | 4 ++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/app/components/AvailableModulesMenu.jsx b/src/app/components/AvailableModulesMenu.jsx index de4a6e17..1ae53a44 100644 --- a/src/app/components/AvailableModulesMenu.jsx +++ b/src/app/components/AvailableModulesMenu.jsx @@ -39,13 +39,17 @@ const GRPCAT = { 'ml': 'lasers', 'c': 'projectiles', 'mc': 'projectiles', + 'advmc': 'projectiles', 'axmc': 'experimental', + 'axmce': 'experimental', + 'ntp': 'experimental', 'fc': 'projectiles', 'rfl': 'experimental', 'pa': 'projectiles', 'rg': 'projectiles', 'mr': 'ordnance', 'axmr': 'experimental', + 'axmre': 'experimental', 'rcpl': 'experimental', 'dtl': 'experimental', 'tbsc': 'experimental', @@ -104,7 +108,7 @@ const CATEGORIES = { // Hardpoints 'lasers': ['pl', 'ul', 'bl'], - 'projectiles': ['mc', 'c', 'fc', 'pa', 'rg'], + 'projectiles': ['mc', 'advmc', 'c', 'fc', 'pa', 'rg'], 'ordnance': ['mr', 'tp', 'nl'], // Utilities 'sb': ['sb'], @@ -113,7 +117,7 @@ const CATEGORIES = { 'defence': ['ch', 'po', 'ec'], 'scanners': ['sc', 'ss', 'cs', 'kw', 'ws'], // Overloaded with internal scanners // Experimental - 'experimental': ['axmc', 'axmr', 'rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',], + 'experimental': ['axmc', 'axmce', 'axmr', 'axmre', 'ntp','rfl', 'tbrfl', 'tbsc', 'tbem', 'xs', 'sfn', 'rcpl', 'dtl', 'rsl', 'mahr',], 'weapon stabilizers': ['ews'], // Guardian 'guardian': ['gpp', 'gpd', 'gpc', 'ggc', 'gsrp', 'gfsb', 'ghrp', 'gmrp', 'gsc'], diff --git a/src/app/i18n/en.json b/src/app/i18n/en.json index fd6b3f0b..e232373f 100644 --- a/src/app/i18n/en.json +++ b/src/app/i18n/en.json @@ -109,11 +109,14 @@ "kw": "Kill Warrant Scanner", "ls": "Life Support", "mc": "Multi-cannon", + "advmc": "Multi-cannon (Advanced)", "axmc": "AX Multi-cannon", + "axmce": "AX Multi-cannon (Enhanced)", "ml": "Mining Laser", "mlc": "Multi Limpet Controller", "mr": "Missile Rack", "axmr": "AX Missile Rack", + "axmre": "AX Missile Rack (Enhanced)", "ews": "Experimental Weapon Stabilizer", "mrp": "Module Reinforcement Package", "nl": "Mine Launcher", @@ -159,6 +162,7 @@ "sua": "Supercruise Assist", "t": "thrusters", "tp": "Torpedo Pylon", + "ntp": "Nanite Torpedo Pylon", "ul": "Burst Laser", "Send To EDEngineer": "Send To EDEngineer", "Send To EDOMH": "Send To EDOMH",