From 9556f28ba469d6482f5d0b2be7a416b17503ed99 Mon Sep 17 00:00:00 2001 From: Cmdr McDonald Date: Sat, 12 Nov 2016 12:02:52 +0000 Subject: [PATCH] Add ability to import directly from companion API output --- ChangeLog.md | 3 + .../fixtures/companion-api-import-1.json | 1288 +++++++++++++++++ __tests__/test-import.js | 17 + src/app/components/ModalImport.jsx | 23 +- src/app/shipyard/Ship.js | 26 +- src/app/utils/CompanionApiUtils.js | 317 ++++ 6 files changed, 1660 insertions(+), 14 deletions(-) create mode 100644 __tests__/fixtures/companion-api-import-1.json create mode 100644 src/app/utils/CompanionApiUtils.js diff --git a/ChangeLog.md b/ChangeLog.md index db3c3fef..069f7fa3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,8 @@ * Add initial loadout passenger cabins for Beluga * Add initial loadout passenger cabins for Orca * Update costs and initial loadouts for Keelback and Type-7 + * Add resistances for hull reinforcement packages + * Added modifier actions to create modifications from raw data * Show modification icon for modified modules * Take modifications in to account when deciding whether to issue a warning on a standard module * Fix hardpoint comparison DPS number when selecting an alternate module @@ -14,3 +16,4 @@ * Enable boost display even if power distributor is disabled * Calculate breakdown of ship offensive and defensive stats * Add 'Offence summary' and 'Defence summary' components + * Add ability to import directly from companion API output diff --git a/__tests__/fixtures/companion-api-import-1.json b/__tests__/fixtures/companion-api-import-1.json new file mode 100644 index 00000000..9546ffae --- /dev/null +++ b/__tests__/fixtures/companion-api-import-1.json @@ -0,0 +1,1288 @@ +{ + "cargo": { + "capacity": 128, + "items": [ + { + "commodity": "ancientrelic", + "marked": 0, + "masq": null, + "mission": null, + "origin": 2, + "owner": 822212, + "powerplayOrigin": null, + "qty": 1, + "value": 0, + "xyz": null + } + ], + "lock": 359606096, + "qty": 1, + "ts": { + "sec": 1478848090, + "usec": 568000 + } + }, + "cockpitBreached": false, + "free": false, + "fuel": { + "main": { + "capacity": 96, + "level": 96 + }, + "reserve": { + "capacity": 1.13, + "level": 1.13 + }, + "superchargedFSD": 0 + }, + "health": { + "hull": 1000000, + "integrity": 30447, + "paintwork": 30448, + "shield": 1000000, + "shieldup": true + }, + "id": 21, + "modules": { + "Armour": { + "module": { + "free": false, + "health": 1000000, + "id": 128049372, + "name": "Federation_Corvette_Armour_Grade3", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 143796630 + } + }, + "Bobble01": [], + "Bobble02": [], + "Bobble03": [], + "Bobble04": [], + "Bobble05": [], + "Bobble06": [], + "Bobble07": [], + "Bobble08": [], + "Bobble09": [], + "Bobble10": [], + "Decal1": { + "module": { + "free": false, + "health": 1000000, + "id": 128667742, + "name": "Decal_Combat_Deadly", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 0 + } + }, + "Decal2": { + "module": { + "free": false, + "health": 1000000, + "id": 128667757, + "name": "Decal_Explorer_Ranger", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 0 + } + }, + "Decal3": { + "module": { + "free": false, + "health": 1000000, + "id": 128667750, + "name": "Decal_Trade_Tycoon", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 0 + } + }, + "EngineColour": [], + "FrameShiftDrive": { + "module": { + "free": false, + "health": 1000000, + "id": 128064127, + "modifiers": { + "engineerID": 300100, + "id": 3904, + "modifiers": [ + { + "name": "mod_mass", + "type": 1, + "value": 0.42523837089539 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.2126482129097 + }, + { + "name": "mod_passive_power", + "type": 1, + "value": 0.23241454362869 + }, + { + "name": "mod_fsd_optimised_mass", + "type": 1, + "value": 0.46293264627457 + }, + { + "name": "mod_fsd_optimised_mass", + "type": 2, + "value": 0.040929391980171 + }, + { + "name": "trade_mass_health", + "type": 2, + "value": 0.088846378028393 + } + ], + "moduleTags": [ + 16 + ], + "recipeID": 128673694, + "slotIndex": 47 + }, + "name": "Int_Hyperdrive_Size6_Class5", + "on": true, + "priority": 0, + "recipeLevel": 5, + "recipeName": "FSD_LongRange", + "recipeValue": 0, + "unloaned": 0, + "value": 13752602 + } + }, + "FuelTank": { + "module": { + "free": false, + "health": 1000000, + "id": 128064350, + "name": "Int_FuelTank_Size5_Class3", + "on": true, + "priority": 1, + "unloaned": 83090, + "value": 83090 + } + }, + "HugeHardpoint1": { + "module": { + "ammo": { + "clip": 1, + "hopper": 1 + }, + "free": false, + "health": 1000000, + "id": 128681994, + "modifiers": { + "engineerID": 300180, + "id": 3380, + "modifiers": [ + { + "name": "mod_weapon_range", + "type": 1, + "value": 0.19140657782555 + }, + { + "name": "mod_weapon_damage", + "type": 1, + "value": -0.043880753219128 + }, + { + "name": "mod_weapon_active_power", + "type": 1, + "value": 0.08408235758543 + }, + { + "name": "mod_mass", + "type": 1, + "value": 0.042046930640936 + }, + { + "name": "mod_passive_power", + "type": 2, + "value": -0.094719000160694 + }, + { + "name": "special_thermalshock", + "type": 3, + "value": 1 + } + ], + "moduleTags": [ + 1, + 4 + ], + "recipeID": 128673335, + "slotIndex": 26 + }, + "name": "Hpt_BeamLaser_Gimbal_Huge", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "Weapon_LongRange", + "recipeValue": 0, + "unloaned": 0, + "value": 7871544 + } + }, + "HugeHardpoint2": { + "module": { + "ammo": { + "clip": 1, + "hopper": 1 + }, + "free": false, + "health": 1000000, + "id": 128681994, + "name": "Hpt_BeamLaser_Gimbal_Huge", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 7871544 + } + }, + "LargeHardpoint1": { + "module": { + "ammo": { + "clip": 90, + "hopper": 2100 + }, + "free": false, + "health": 1000000, + "id": 128049461, + "modifiers": { + "engineerID": 300260, + "id": 4699, + "modifiers": [ + { + "name": "mod_weapon_damage", + "type": 1, + "value": 0.24115231633186 + }, + { + "name": "mod_weapon_active_power", + "type": 1, + "value": 0.61637383699417 + }, + { + "name": "mod_weapon_burst_interval", + "type": 1, + "value": -0.11122596263885 + }, + { + "name": "mod_weapon_jitter_radius", + "type": 1, + "value": 0.56468063592911 + }, + { + "name": "mod_weapon_active_heat", + "type": 1, + "value": 0.12204115837812 + }, + { + "name": "mod_weapon_hardness_piercing", + "type": 2, + "value": 0.033723440021276 + }, + { + "name": "mod_weapon_hardness_piercing", + "type": 2, + "value": -0.036045636981726 + }, + { + "name": "special_incendiary_rounds", + "type": 3, + "value": 1 + } + ], + "moduleTags": [ + 1, + 8 + ], + "recipeID": 128673502, + "slotIndex": 28 + }, + "name": "Hpt_MultiCannon_Gimbal_Large", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "Weapon_Overcharged", + "recipeValue": 0, + "unloaned": 0, + "value": 520593 + } + }, + "LifeSupport": { + "module": { + "free": false, + "health": 1000000, + "id": 128064159, + "name": "Int_LifeSupport_Size5_Class2", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 67528 + } + }, + "MainEngines": { + "module": { + "free": false, + "health": 1000000, + "id": 128064094, + "modifiers": { + "engineerID": 300100, + "id": 3920, + "modifiers": [ + { + "name": "mod_engine_mass_curve_multiplier", + "type": 1, + "value": 0.16290386021137 + }, + { + "name": "mod_engine_heat", + "type": 1, + "value": 0.51375859975815 + }, + { + "name": "mod_passive_power", + "type": 1, + "value": 0.068986810743809 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.12497692555189 + }, + { + "name": "mod_engine_mass_curve", + "type": 1, + "value": -0.046525910496712 + }, + { + "name": "mod_engine_mass_curve_multiplier", + "type": 2, + "value": 0.0091453474014997 + }, + { + "name": "mod_mass", + "type": 2, + "value": 0.090709120035172 + } + ], + "moduleTags": [ + 17 + ], + "recipeID": 128673657, + "slotIndex": 46 + }, + "name": "Int_Engine_Size7_Class2", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "Engine_Dirty", + "recipeValue": 0, + "unloaned": 0, + "value": 1614658 + } + }, + "MediumHardpoint1": { + "module": { + "ammo": { + "clip": 90, + "hopper": 2100 + }, + "free": false, + "health": 1000000, + "id": 128049463, + "modifiers": { + "engineerID": 300260, + "id": 4729, + "modifiers": [ + { + "name": "mod_weapon_damage", + "type": 1, + "value": 0.24256283044815 + }, + { + "name": "mod_weapon_active_power", + "type": 1, + "value": 0.61838209629059 + }, + { + "name": "mod_weapon_burst_interval", + "type": 1, + "value": -0.10679690539837 + }, + { + "name": "mod_weapon_jitter_radius", + "type": 1, + "value": 0.56370949745178 + }, + { + "name": "mod_weapon_active_heat", + "type": 1, + "value": 0.10164558887482 + }, + { + "name": "trade_weapon_damage_weapon_active_power", + "type": 2, + "value": 0.052759505808353 + }, + { + "name": "mod_health", + "type": 2, + "value": -0.032706737518311 + } + ], + "moduleTags": [ + 1, + 8 + ], + "recipeID": 128673502, + "slotIndex": 29 + }, + "name": "Hpt_MultiCannon_Turret_Medium", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "Weapon_Overcharged", + "recipeValue": 0, + "unloaned": 0, + "value": 1292800 + } + }, + "MediumHardpoint2": { + "module": { + "ammo": { + "clip": 90, + "hopper": 2100 + }, + "free": false, + "health": 1000000, + "id": 128049463, + "modifiers": { + "engineerID": 300260, + "id": 4731, + "modifiers": [ + { + "name": "mod_weapon_damage", + "type": 1, + "value": 0.2441771030426 + }, + { + "name": "mod_weapon_active_power", + "type": 1, + "value": 0.69319069385529 + }, + { + "name": "mod_weapon_burst_interval", + "type": 1, + "value": -0.10855937004089 + }, + { + "name": "mod_weapon_jitter_radius", + "type": 1, + "value": 0.55067348480225 + }, + { + "name": "mod_weapon_active_heat", + "type": 1, + "value": 0.11971006542444 + }, + { + "name": "trade_mass_health", + "type": 2, + "value": 0.0050421766936779 + }, + { + "name": "mod_passive_power", + "type": 2, + "value": 0.03293726965785 + } + ], + "moduleTags": [ + 1, + 8 + ], + "recipeID": 128673502, + "slotIndex": 30 + }, + "name": "Hpt_MultiCannon_Turret_Medium", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "Weapon_Overcharged", + "recipeValue": 0, + "unloaned": 0, + "value": 1292800 + } + }, + "PaintJob": { + "module": { + "free": false, + "health": 1000000, + "id": 128732313, + "name": "PaintJob_Federation_Corvette_Militaire_Sand", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 0 + } + }, + "PlanetaryApproachSuite": { + "module": { + "free": false, + "health": 1000000, + "id": 128672317, + "name": "Int_PlanetApproachSuite", + "on": true, + "priority": 1, + "unloaned": 425, + "value": 425 + } + }, + "PowerDistributor": { + "module": { + "free": false, + "health": 1000000, + "id": 128064217, + "modifiers": { + "engineerID": 300180, + "id": 3376, + "modifiers": [ + { + "name": "mod_powerdistributor_weapon_charge", + "type": 1, + "value": 0.27520388364792 + }, + { + "name": "mod_powerdistributor_weapon_rate", + "type": 1, + "value": 0.10803784430027 + }, + { + "name": "mod_powerdistributor_system_charge", + "type": 1, + "value": -0.13366678357124 + }, + { + "name": "mod_powerdistributor_system_rate", + "type": 1, + "value": -0.029957808554173 + }, + { + "name": "mod_powerdistributor_engine_charge", + "type": 1, + "value": -0.085655435919762 + }, + { + "name": "mod_powerdistributor_engine_rate", + "type": 1, + "value": -0.12185442447662 + }, + { + "name": "mod_health", + "type": 2, + "value": 0.056631729006767 + }, + { + "name": "mod_powerdistributor_weapon_charge", + "type": 2, + "value": 0.0055946228094399 + }, + { + "name": "mod_powerdistributor_global_rate", + "type": 2, + "value": -0.011308163404465 + } + ], + "moduleTags": [ + 19 + ], + "recipeID": 128673752, + "slotIndex": 49 + }, + "name": "Int_PowerDistributor_Size8_Class5", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "PowerDistributor_PriorityWeapons", + "recipeValue": 0, + "unloaned": 0, + "value": 23161983 + } + }, + "PowerPlant": { + "module": { + "free": false, + "health": 1000000, + "id": 128064067, + "modifiers": { + "engineerID": 300100, + "id": 3942, + "modifiers": [ + { + "name": "mod_powerplant_power", + "type": 1, + "value": 0.081056952476501 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.0084498468786478 + }, + { + "name": "mod_powerplant_heat", + "type": 1, + "value": 0.010082358494401 + }, + { + "name": "mod_powerplant_power", + "type": 2, + "value": 0.043923784047365 + } + ], + "moduleTags": [ + 18 + ], + "recipeID": 128673765, + "slotIndex": 45 + }, + "name": "Int_Powerplant_Size8_Class5", + "on": true, + "priority": 1, + "recipeLevel": 1, + "recipeName": "PowerPlant_Boosted", + "recipeValue": 0, + "unloaned": 0, + "value": 138198514 + } + }, + "Radar": { + "module": { + "free": false, + "health": 1000000, + "id": 128064254, + "name": "Int_Sensors_Size8_Class2", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 1482367 + } + }, + "Slot01_Size7": { + "module": { + "free": false, + "health": 1000000, + "id": 128064344, + "name": "Int_CargoRack_Size7_Class1", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 1001657 + } + }, + "Slot02_Size7": { + "module": { + "free": false, + "health": 1000000, + "id": 128671329, + "modifiers": { + "engineerID": 300160, + "id": 1783, + "modifiers": [ + { + "name": "mod_shield_mass_curve_multiplier", + "type": 1, + "value": 0.11387270689011 + }, + { + "name": "mod_shield_global_mult", + "type": 1, + "value": -0.084811583161354 + }, + { + "name": "mod_shield_broken_regen", + "type": 1, + "value": -0.14731486141682 + }, + { + "name": "mod_shield_normal_regen", + "type": 1, + "value": -0.036414299160242 + }, + { + "name": "mod_shield_energy_per_regen", + "type": 1, + "value": 0.022321036085486 + }, + { + "name": "trade_shield_curve_shield_curve_mult", + "type": 2, + "value": -0.11246068775654 + }, + { + "name": "mod_shield_broken_regen", + "type": 2, + "value": -0.063284143805504 + } + ], + "moduleTags": [ + 15 + ], + "recipeID": 128673837, + "slotIndex": 53 + }, + "name": "Int_ShieldGenerator_Size7_Class5_Strong", + "on": true, + "priority": 0, + "recipeLevel": 3, + "recipeName": "ShieldGenerator_Reinforced", + "recipeValue": 0, + "unloaned": 0, + "value": 69240302 + } + }, + "Slot03_Size7": { + "module": { + "ammo": { + "clip": 1, + "hopper": 4 + }, + "free": false, + "health": 1000000, + "id": 128064332, + "modifiers": { + "engineerID": 300160, + "id": 1789, + "modifiers": [ + { + "name": "mod_shieldcell_spin_up", + "type": 1, + "value": -0.086017057299614 + }, + { + "name": "mod_shieldcell_duration", + "type": 1, + "value": -0.047102440148592 + }, + { + "name": "mod_shieldcell_shield_units", + "type": 1, + "value": 0.021292699500918 + }, + { + "name": "mod_boot_time", + "type": 1, + "value": 0.071096949279308 + }, + { + "name": "mod_shieldcell_spin_up", + "type": 2, + "value": -0.018995799124241 + } + ], + "moduleTags": [ + 24 + ], + "recipeID": 128673805, + "slotIndex": 54 + }, + "name": "Int_ShieldCellBank_Size7_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldCellBank_Rapid", + "recipeValue": 0, + "unloaned": 0, + "value": 8272137 + } + }, + "Slot04_Size6": { + "module": { + "free": false, + "health": 1000000, + "id": 128064351, + "name": "Int_FuelTank_Size6_Class3", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 290341 + } + }, + "Slot05_Size6": { + "module": { + "free": false, + "health": 1000000, + "id": 128666681, + "name": "Int_FuelScoop_Size6_Class5", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 24449069 + } + }, + "Slot06_Size5": { + "module": { + "ammo": { + "clip": 1, + "hopper": 3 + }, + "free": false, + "health": 1000000, + "id": 128064322, + "modifiers": { + "engineerID": 300160, + "id": 1788, + "modifiers": [ + { + "name": "mod_shieldcell_spin_up", + "type": 1, + "value": -0.084282241761684 + }, + { + "name": "mod_shieldcell_duration", + "type": 1, + "value": -0.069141998887062 + }, + { + "name": "mod_shieldcell_shield_units", + "type": 1, + "value": 0.025862643495202 + }, + { + "name": "mod_boot_time", + "type": 1, + "value": 0.081815980374813 + }, + { + "name": "mod_shieldcell_duration", + "type": 2, + "value": 0.041702415794134 + } + ], + "moduleTags": [ + 24 + ], + "recipeID": 128673805, + "slotIndex": 57 + }, + "name": "Int_ShieldCellBank_Size5_Class5", + "on": false, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldCellBank_Rapid", + "recipeValue": 0, + "unloaned": 0, + "value": 1055120 + } + }, + "Slot07_Size5": { + "module": { + "free": false, + "health": 1000000, + "id": 128727930, + "name": "Int_FighterBay_Size5_Class1", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 575643 + } + }, + "Slot08_Size4": { + "module": { + "free": false, + "health": 1000000, + "id": 128672289, + "name": "Int_BuggyBay_Size2_Class2", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 18360 + } + }, + "Slot09_Size4": { + "module": { + "free": false, + "health": 1000000, + "id": 128666722, + "modifiers": { + "engineerID": 300100, + "id": 2026, + "modifiers": [ + { + "name": "mod_fsdinterdictor_range", + "type": 1, + "value": 0.086278550326824 + }, + { + "name": "mod_mass", + "type": 1, + "value": 0.07684014737606 + }, + { + "name": "mod_passive_power", + "type": 1, + "value": 0.13557997345924 + }, + { + "name": "mod_fsdinterdictor_facing_limit", + "type": 1, + "value": -0.07848084717989 + } + ], + "moduleTags": [ + 23 + ], + "recipeID": 128673680, + "slotIndex": 60 + }, + "name": "Int_FSDInterdictor_Size3_Class5", + "on": true, + "priority": 1, + "recipeLevel": 1, + "recipeName": "FSDinterdictor_LongRange", + "recipeValue": 0, + "unloaned": 0, + "value": 6477408 + } + }, + "Slot10_Size3": { + "module": { + "free": false, + "health": 1000000, + "id": 128663561, + "name": "Int_StellarBodyDiscoveryScanner_Advanced", + "on": true, + "priority": 1, + "unloaned": 0, + "value": 1313250 + } + }, + "SmallHardpoint1": { + "module": { + "ammo": { + "clip": 1, + "hopper": 1 + }, + "free": false, + "health": 1000000, + "id": 128049388, + "name": "Hpt_PulseLaser_Turret_Small", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 26000 + } + }, + "SmallHardpoint2": { + "module": { + "ammo": { + "clip": 1, + "hopper": 1 + }, + "free": false, + "health": 1000000, + "id": 128049388, + "name": "Hpt_PulseLaser_Turret_Small", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 26000 + } + }, + "TinyHardpoint1": { + "module": { + "free": false, + "health": 1000000, + "id": 128668536, + "modifiers": { + "engineerID": 300100, + "id": 3948, + "modifiers": [ + { + "name": "mod_passive_power", + "type": 1, + "value": 0.065562553703785 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 1, + "value": -0.046933718025684 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.037372145801783 + }, + { + "name": "mod_passive_power", + "type": 2, + "value": -0.054788526147604 + } + ], + "moduleTags": [ + 22 + ], + "recipeID": 128673790, + "slotIndex": 33 + }, + "name": "Hpt_ShieldBooster_Size0_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldBooster_Resistive", + "recipeValue": 0, + "unloaned": 0, + "value": 238850 + } + }, + "TinyHardpoint2": { + "module": { + "free": false, + "health": 1000000, + "id": 128668536, + "modifiers": { + "engineerID": 300100, + "id": 3949, + "modifiers": [ + { + "name": "mod_passive_power", + "type": 1, + "value": 0.050724714994431 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 1, + "value": -0.042270515114069 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.030213074758649 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 2, + "value": -0.0012421812862158 + } + ], + "moduleTags": [ + 22 + ], + "recipeID": 128673790, + "slotIndex": 34 + }, + "name": "Hpt_ShieldBooster_Size0_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldBooster_Resistive", + "recipeValue": 0, + "unloaned": 0, + "value": 238850 + } + }, + "TinyHardpoint3": { + "module": { + "free": false, + "health": 1000000, + "id": 128668536, + "modifiers": { + "engineerID": 300100, + "id": 3950, + "modifiers": [ + { + "name": "mod_passive_power", + "type": 1, + "value": 0.049866359680891 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 1, + "value": -0.046149685978889 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.024229854345322 + }, + { + "name": "mod_defencemodifier_shield_mult", + "type": 2, + "value": 0.036322306841612 + } + ], + "moduleTags": [ + 22 + ], + "recipeID": 128673790, + "slotIndex": 35 + }, + "name": "Hpt_ShieldBooster_Size0_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldBooster_Resistive", + "recipeValue": 0, + "unloaned": 0, + "value": 238850 + } + }, + "TinyHardpoint4": { + "module": { + "free": false, + "health": 1000000, + "id": 128668536, + "modifiers": { + "engineerID": 300100, + "id": 3951, + "modifiers": [ + { + "name": "mod_passive_power", + "type": 1, + "value": 0.073374435305595 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 1, + "value": -0.046808175742626 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.037895452231169 + }, + { + "name": "mod_defencemodifier_shield_kinetic_mult", + "type": 2, + "value": -0.0045657334849238 + } + ], + "moduleTags": [ + 22 + ], + "recipeID": 128673790, + "slotIndex": 36 + }, + "name": "Hpt_ShieldBooster_Size0_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldBooster_Resistive", + "recipeValue": 0, + "unloaned": 0, + "value": 238850 + } + }, + "TinyHardpoint5": { + "module": { + "free": false, + "health": 1000000, + "id": 128668536, + "modifiers": { + "engineerID": 300100, + "id": 3954, + "modifiers": [ + { + "name": "mod_passive_power", + "type": 1, + "value": 0.013932451605797 + }, + { + "name": "mod_defencemodifier_global_shield_mult", + "type": 1, + "value": -0.047755606472492 + }, + { + "name": "mod_health", + "type": 1, + "value": -0.024494780227542 + }, + { + "name": "trade_passive_power_booster_global_mult", + "type": 2, + "value": -0.066611737012863 + } + ], + "moduleTags": [ + 22 + ], + "recipeID": 128673790, + "slotIndex": 37 + }, + "name": "Hpt_ShieldBooster_Size0_Class5", + "on": true, + "priority": 0, + "recipeLevel": 1, + "recipeName": "ShieldBooster_Resistive", + "recipeValue": 0, + "unloaned": 0, + "value": 238850 + } + }, + "TinyHardpoint6": { + "module": { + "ammo": { + "clip": 1, + "hopper": 2 + }, + "free": false, + "health": 1000000, + "id": 128049519, + "name": "Hpt_HeatSinkLauncher_Turret_Tiny", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 2975 + } + }, + "TinyHardpoint7": { + "module": { + "ammo": { + "clip": 1, + "hopper": 2 + }, + "free": false, + "health": 1000000, + "id": 128049519, + "name": "Hpt_HeatSinkLauncher_Turret_Tiny", + "on": false, + "priority": 2, + "unloaned": 0, + "value": 3150 + } + }, + "TinyHardpoint8": { + "module": { + "free": false, + "health": 1000000, + "id": 128662532, + "name": "Hpt_CrimeScanner_Size0_Class3", + "on": true, + "priority": 0, + "unloaned": 0, + "value": 103615 + } + }, + "WeaponColour": [] + }, + "name": "Federation_Corvette", + "oxygenRemaining": 450000, + "passengers": [], + "refinery": null, + "value": { + "cargo": 0, + "hull": 155200708, + "modules": 455056355, + "total": 610257063, + "unloaned": 83515 + } +} diff --git a/__tests__/test-import.js b/__tests__/test-import.js index 4c62b00c..686948b4 100644 --- a/__tests__/test-import.js +++ b/__tests__/test-import.js @@ -196,6 +196,23 @@ describe('Import Modal', function() { }); }); + describe('Import Companion API Build', function() { + + beforeEach(reset); + + it('imports a valid v4 build', function() { + const importData = require('./fixtures/companion-api-import-1'); + pasteText(JSON.stringify(importData)); + + expect(modal.state.importValid).toBeTruthy(); + expect(modal.state.errorMsg).toEqual(null); + expect(modal.state.singleBuild).toBe(true); + clickProceed(); + expect(MockRouter.go.mock.calls.length).toBe(1); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/federal_corvette/2putsFklndzsxf50x0x7l28281919040404040402020l060606f6f65i5iv6v62f.AwRj4zNaZI==.CwJgjBkAw7eyKkI0kA==.H4sIAAAAAAAAA22Qvy5EURDG5+51-+w94uweF0dY--aiICQrwSPYYt9AS3XjARQKiWb1nkCioZEoRKfZmlLES6xCLMb3KSQSzZeZ85v5ZuYE43EkYi9VNd0TEa2M9WKRpstF-HUFaGagmq9-qbrTEDx0Zw6vB++qPq+K5CUqNVpMjMjSHSrme0AL+2ioXnyoZk9I7SacNJ08TESi-qtq3EalOw8wpIMizaISXnHuRRpItbaNXZrlrsjEM3MT3WfgR1N-+BtM7C0m6XDU-en-5VvkV0PgLUS8Rnwb49T6HcRmBV11SkGxXZrU-AAVpkNEKSj2kaju+0QbRJSCYkdAZo9TuDr-ggvNKrmQM7InbB31a1jTFJ9AlIJiDbecnrvB8ckDypZD+Act-nDj31f9Bizb3eiqAQAA?bn=Imported%20Federal%20Corvette'); + }); + }); + describe('Import E:D Shipyard Builds', function() { it('imports a valid builds', function() { diff --git a/src/app/components/ModalImport.jsx b/src/app/components/ModalImport.jsx index 04dd2b87..1d1a56d4 100644 --- a/src/app/components/ModalImport.jsx +++ b/src/app/components/ModalImport.jsx @@ -11,6 +11,7 @@ import * as ModuleUtils from '../shipyard/ModuleUtils'; import { fromDetailedBuild } from '../shipyard/Serializer'; import { Download } from './SvgIcons'; import { outfitURL } from '../utils/UrlGenerators'; +import * as CompanionApiUtils from '../utils/CompanionApiUtils'; const textBuildRegex = new RegExp('^\\[([\\w \\-]+)\\]\n'); const lineRegex = new RegExp('^([\\dA-Z]{1,2}): (\\d)([A-I])[/]?([FGT])?([SD])? ([\\w\\- ]+)'); @@ -112,6 +113,7 @@ export default class ModalImport extends TranslatedComponent { this._importBackup = this._importBackup.bind(this); this._importDetailedArray = this._importDetailedArray.bind(this); this._importTextBuild = this._importTextBuild.bind(this); + this._importCompanionApiBuild = this._importCompanionApiBuild.bind(this); this._validateImport = this._validateImport.bind(this); } @@ -183,6 +185,21 @@ export default class ModalImport extends TranslatedComponent { this.setState({ builds }); } + /** + * Import a build direct from the companion API + * @param {string} build JSON from the companion API information + * @throws {string} if parse/import fails + */ + _importCompanionApiBuild(build) { + const shipModel = CompanionApiUtils.shipModelFromJson(build); + const ship = CompanionApiUtils.shipFromJson(build); + + let builds = {}; + builds[shipModel] = {}; + builds[shipModel]['Imported ' + Ships[shipModel].properties.name] = ship.toString(); + this.setState({ builds, singleBuild: true }); + } + /** * Import a text build from ED Shipyard * @param {string} buildStr Build string @@ -315,7 +332,11 @@ export default class ModalImport extends TranslatedComponent { throw 'Must be an object or array!'; } - if (importData instanceof Array) { // Must be detailed export json + if (importData.cockpitBreached != null) { // Only the companion API has this information + this._importCompanionApiBuild(importData); // Single sihp definition + } else if (importData.ship != null && importData.ship.cockpitBreached != null) { // Only the companion API has this information + this._importCompanionApiBuild(importData.ship); // Complete API dump + } else if (importData instanceof Array) { // Must be detailed export json this._importDetailedArray(importData); } else if (importData.ship && typeof importData.name !== undefined) { // Using JSON from a single ship build export this._importDetailedArray([importData]); // Convert to array with singleobject diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index ad7b1cd1..ad5301a8 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1233,7 +1233,7 @@ export default class Ship { let bulkheadMods = new Array(); if (this.bulkheads.m && this.bulkheads.m.mods) { for (let modKey in this.bulkheads.m.mods) { - bulkheadMods.push(Modifications.modifiers.indexOf(modKey) + ':' + this.bulkheads.m.getModValue(modKey)); + bulkheadMods.push(Modifications.modifications.indexOf(modKey) + ':' + this.bulkheads.m.getModValue(modKey)); } } allMods.push(bulkheadMods.join(';')); @@ -1242,7 +1242,7 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); @@ -1251,7 +1251,7 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); @@ -1260,7 +1260,7 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push(Modifications.modifiers.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); @@ -1283,7 +1283,7 @@ export default class Ship { for (let j = 0; j < mods.length; j++) { let modElements = mods[j].split(':'); if (modElements[0].match('[0-9]+')) { - arr[i][Modifications.modifiers[modElements[0]]] = Number(modElements[1]); + arr[i][Modifications.modifications[modElements[0]]] = Number(modElements[1]); } else { arr[i][modElements[0]] = Number(modElements[1]); } @@ -1297,7 +1297,7 @@ export default class Ship { * This is a binary structure. It starts with a byte that identifies a slot, with bulkheads being ID 0 and moving through * standard modules, hardpoints, and finally internal modules. It then contains one or more modifications, with each * modification being a one-byte modification ID and at two-byte modification value. Modification IDs are based on the array - * in Modifications.modifiers. The list of modifications is terminated by a modification ID of -1. The structure then repeats + * in Modifications.modifications. The list of modifications is terminated by a modification ID of -1. The structure then repeats * for the next module, and the next, and is terminated by a slot ID of -1. * @return {this} The ship instance (for chaining operations) */ @@ -1310,7 +1310,7 @@ export default class Ship { for (let modKey in this.bulkheads.m.mods) { // Filter out invalid modifications if (Modifications.validity['bh'] && Modifications.validity['bh'].indexOf(modKey) != -1) { - bulkheadMods.push({ id: Modifications.modifiers.indexOf(modKey), value: this.bulkheads.m.getModValue(modKey) }); + bulkheadMods.push({ id: Modifications.modifications.indexOf(modKey), value: this.bulkheads.m.getModValue(modKey) }); } } } @@ -1322,7 +1322,7 @@ export default class Ship { for (let modKey in slot.m.mods) { // Filter out invalid modifications if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: slot.m.getModValue(modKey) }); + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); } } } @@ -1335,7 +1335,7 @@ export default class Ship { for (let modKey in slot.m.mods) { // Filter out invalid modifications if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: slot.m.getModValue(modKey) }); + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); } } } @@ -1348,7 +1348,7 @@ export default class Ship { for (let modKey in slot.m.mods) { // Filter out invalid modifications if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: slot.m.getModValue(modKey) }); + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); } } } @@ -1375,7 +1375,7 @@ export default class Ship { for (let slotMod of slot) { buffer.writeInt8(slotMod.id, curpos++); buffer.writeInt32LE(slotMod.value, curpos); - // console.log('ENCODE Slot ' + i + ': ' + Modifications.modifiers[slotMod.id] + ' = ' + slotMod.value); + // console.log('ENCODE Slot ' + i + ': ' + Modifications.modifications[slotMod.id] + ' = ' + slotMod.value); curpos += 4; } buffer.writeInt8(-1, curpos++); @@ -1408,8 +1408,8 @@ export default class Ship { while (modificationId != -1) { let modificationValue = buffer.readInt32LE(curpos); curpos += 4; - // console.log('DECODE Slot ' + slot + ': ' + Modifications.modifiers[modificationId] + ' = ' + modificationValue); - modifications[Modifications.modifiers[modificationId]] = modificationValue; + // console.log('DECODE Slot ' + slot + ': ' + Modifications.modifications[modificationId] + ' = ' + modificationValue); + modifications[Modifications.modifications[modificationId]] = modificationValue; modificationId = buffer.readInt8(curpos++); } arr[slot] = modifications; diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js new file mode 100644 index 00000000..c43bf815 --- /dev/null +++ b/src/app/utils/CompanionApiUtils.js @@ -0,0 +1,317 @@ +import React from 'react'; +import { Modifications, Modules, Ships } from 'coriolis-data/dist'; +import Module from '../shipyard/Module'; +import Ship from '../shipyard/Ship'; +import * as ModuleUtils from '../shipyard/ModuleUtils'; + + +// mapping from fd's ship model names to coriolis' +const SHIP_FD_NAME_TO_CORIOLIS_NAME = { + 'Adder': 'adder', + 'Anaconda': 'anaconda', + 'Asp': 'asp', + 'Asp_Scout': 'asp_scout', + 'BelugaLiner': 'beluga', + 'CobraMkIII': 'cobra_mk_iii', + 'CobraMkIV': 'cobra_mk_iv', + 'Cutter': 'imperial_cutter', + 'DiamondBack': 'diamondback_explorer', + 'DiamondBackXL': 'diamondback', + 'Eagle': 'eagle', + 'Empire_Courier': 'imperial_courier', + 'Empire_Eagle': 'imperial_eagle', + 'Empire_Trader': 'imperial_clipper', + 'Federation_Corvette': 'federal_corvette', + 'Federation_Dropship': 'federal_dropship', + 'Federation_Dropship_MkII': 'federal_assault_ship', + 'Federation_Gunship': 'federal_gunship', + 'FerDeLance': 'fer_de_lance', + 'Hauler': 'hauler', + 'Independant_Trader': 'keelback', + 'Orca': 'orca', + 'Python': 'python', + 'SideWinder': 'sidewinder', + 'Type6': 'type_6_transporter', + 'Type7': 'type_7_transport', + 'Type9': 'type_9_heavy', + 'Viper': 'viper', + 'Viper_MKIV': 'viper_mk_iv', + 'Vulture': 'vulture' +}; + +// Mapping from hardpoint class to name in companion API +const HARDPOINT_NUM_TO_CLASS = { + 0: 'Tiny', + 1: 'Small', + 2: 'Medium', + 3: 'Large', + 4: 'Huge' +}; + + +/** + * Obtain a module given its ED ID + * @param {Integer} edId the Elite ID of the module + * @return {Module} the module + */ +function _moduleFromEdId(edId) { + if (!edId) return null; + + // Check standard modules + for (const grp in Modules.standard) { + if (Modules.standard.hasOwnProperty(grp)) { + for (const i in Modules.standard[grp]) { + if (Modules.standard[grp][i].edID === edId) { + // Found it + return new Module({ template: Modules.standard[grp][i] }); + } + } + } + } + + // Check hardpoint modules + for (const grp in Modules.hardpoints) { + if (Modules.hardpoints.hasOwnProperty(grp)) { + for (const i in Modules.hardpoints[grp]) { + if (Modules.hardpoints[grp][i].edID === edId) { + // Found it + return new Module({ template: Modules.hardpoints[grp][i] }); + } + } + } + } + + // Check internal modules + for (const grp in Modules.internal) { + if (Modules.internal.hasOwnProperty(grp)) { + for (const i in Modules.internal[grp]) { + if (Modules.internal[grp][i].edID === edId) { + // Found it + return new Module({ template: Modules.internal[grp][i] }); + } + } + } + } + + // Not found + return null; +} + +/** + * Obtain the model of a ship given its ED name + * @param {string} edName the Elite name of the ship + * @return {string} the Coriolis model of the ship + */ +function _shipModelFromEDName(edName) { + return SHIP_FD_NAME_TO_CORIOLIS_NAME[edName]; +} + +/** + * Obtain a ship's model from the companion API JSON + * @param {object} json the companion API JSON + * @return {string} the Coriolis model of the ship + */ +export function shipModelFromJson(json) { + return _shipModelFromEDName(json.name); +} + +/** + * Build a ship from the companion API JSON + * @param {object} json the companion API JSON + * @return {Ship} the built ship + */ +export function shipFromJson(json) { + // Start off building a basic ship + const shipModel = shipModelFromJson(json); + if (!shipModel) { + throw 'No such ship found: "' + json.name + '"'; + } + const shipTemplate = Ships[shipModel]; + + let ship = new Ship(shipModel, shipTemplate.properties, shipTemplate.slots); + ship.buildWith(null); + + // Set the cargo hatch. We don't have any information on it so guess it's priority 5 and disabled + ship.cargoHatch.enabled = false; + ship.cargoHatch.priority = 4; + + // Add the bulkheads + const armourJson = json.modules.Armour.module; + if (armourJson.name.endsWith('_Armour_Grade1')) { + ship.useBulkhead(0, true); + } else if (armourJson.name.endsWith('_Armour_Grade2')) { + ship.useBulkhead(1, true); + } else if (armourJson.name.endsWith('_Armour_Grade3')) { + ship.useBulkhead(2, true); + } else if (armourJson.name.endsWith('_Armour_Mirrored')) { + ship.useBulkhead(3, true); + } else if (armourJson.name.endsWith('_Armour_Reactive')) { + ship.useBulkhead(4, true); + } else { + throw 'Unknown bulkheads "' + armourJson.name + '"'; + } + ship.bulkheads.enabled = true; + + // Add the standard modules + // Power plant + const powerplantJson = json.modules.PowerPlant.module; + const powerplant = _moduleFromEdId(powerplantJson.id); + if (powerplantJson.modifiers) _addModifications(powerplant, powerplantJson.modifiers); + ship.use(ship.standard[0], powerplant, true); + ship.standard[0].enabled = powerplantJson.on === true; + ship.standard[0].priority = powerplantJson.priority + 1; + + // Thrusters + const thrustersJson = json.modules.MainEngines.module; + const thrusters = _moduleFromEdId(thrustersJson.id); + if (thrustersJson.modifiers) _addModifications(thrusters, thrustersJson.modifiers); + ship.use(ship.standard[1], thrusters, true); + ship.standard[1].enabled = thrustersJson.on === true; + ship.standard[1].priority = thrustersJson.priority + 1; + + // FSD + const frameshiftdriveJson = json.modules.FrameShiftDrive.module; + const frameshiftdrive = _moduleFromEdId(frameshiftdriveJson.id); + if (frameshiftdriveJson.modifiers) _addModifications(frameshiftdrive, frameshiftdriveJson.modifiers); + ship.use(ship.standard[2], frameshiftdrive, true); + ship.standard[2].enabled = frameshiftdriveJson.on === true; + ship.standard[2].priority = frameshiftdriveJson.priority + 1; + + // Life support + const lifesupportJson = json.modules.LifeSupport.module; + const lifesupport = _moduleFromEdId(lifesupportJson.id); + if (lifesupportJson.modifiers)_addModifications(lifesupport, lifesupportJson.modifiers); + ship.use(ship.standard[3], lifesupport, true); + ship.standard[3].enabled = lifesupportJson.on === true; + ship.standard[3].priority = lifesupportJson.priority + 1; + + // Power distributor + const powerdistributorJson = json.modules.PowerDistributor.module; + const powerdistributor = _moduleFromEdId(powerdistributorJson.id); + if (powerdistributorJson.modifiers) _addModifications(powerdistributor, powerdistributorJson.modifiers); + ship.use(ship.standard[4], powerdistributor, true); + ship.standard[4].enabled = powerdistributorJson.on === true; + ship.standard[4].priority = powerdistributorJson.priority + 1; + + // Sensors + const sensorsJson = json.modules.Radar.module; + const sensors = _moduleFromEdId(sensorsJson.id); + if (sensorsJson.modifiers) _addModifications(sensors, sensorsJson.modifiers); + ship.use(ship.standard[5], sensors, true); + ship.standard[5].enabled = sensorsJson.on === true; + ship.standard[5].priority = sensorsJson.priority + 1; + + // Fuel tank + const fueltankJson = json.modules.FuelTank.module; + const fueltank = _moduleFromEdId(fueltankJson.id); + ship.use(ship.standard[6], fueltank, true); + ship.standard[6].enabled = true; + ship.standard[6].priority = 1; + + // Add hardpoints + let hardpointClassNum = -1; + let hardpointSlotNum = -1; + let hardpointArrayNum = 0; + for (let i in shipTemplate.slots.hardpoints) { + if (shipTemplate.slots.hardpoints[i] === hardpointClassNum) { + // Another slot of the same class + hardpointSlotNum++; + } else { + // The first slot of a new class + hardpointClassNum = shipTemplate.slots.hardpoints[i]; + hardpointSlotNum = 1; + } + + // Now that we know what we're looking for, find it + const hardpointName = HARDPOINT_NUM_TO_CLASS[hardpointClassNum] + 'Hardpoint' + hardpointSlotNum; + const hardpointSlot = json.modules[hardpointName]; + if (!hardpointSlot.module) { + // No module + } else { + const hardpointJson = hardpointSlot.module; + const hardpoint = _moduleFromEdId(hardpointJson.id); + if (hardpointJson.modifiers) _addModifications(hardpoint, hardpointJson.modifiers); + ship.use(ship.hardpoints[hardpointArrayNum], hardpoint, true); + ship.hardpoints[hardpointArrayNum].enabled = hardpointJson.on === true; + ship.hardpoints[hardpointArrayNum].priority = hardpointJson.priority; + } + hardpointArrayNum++; + } + + // Add internal compartments + let internalClassNum = -1; + let internalSlotNum = 1; + for (let i in shipTemplate.slots.internal) { + const internalClassNum = shipTemplate.slots.internal[i]; + + let internalSlot = null; + while (internalSlot === null && internalSlotNum < 99) { + // Slot numbers are not contiguous so handle skips + const internalName = 'Slot' + (internalSlotNum < 9 ? '0' : '') + internalSlotNum + '_Size' + internalClassNum; + if (json.modules[internalName]) { + internalSlot = json.modules[internalName]; + } else { + internalSlotNum++; + } + } + if (!internalSlot.module) { + // No module + } else { + const internalJson = internalSlot.module; + const internal = _moduleFromEdId(internalJson.id); + if (internalJson.modifiers) _addModifications(internal, internalJson.modifiers); + ship.use(ship.internal[i], internal, true); + ship.internal[i].enabled = internalJson.on === true; + ship.internal[i].priority = internalJson.priority; + } + } + + // Now update the ship's codes before returning it + return ship.updatePowerPrioritesString().updatePowerEnabledString().updateModificationsString(); +} + +/** + * Add the modifications for a module + * @param {Module} module the module + * @param {Object} modifiers the modifiers + */ +function _addModifications(module, modifiers) { + if (!modifiers || !modifiers.modifiers) return; + + for (const i in modifiers.modifiers) { + // Look up the modifiers to find what we need to do + const modifierActions = Modifications.modifierActions[modifiers.modifiers[i].name]; + const value = modifiers.modifiers[i].value; + + // Carry out the required changes + for (const action in modifierActions) { + const actionValue = modifierActions[action] * value; + let mod = module.getModValue(action); + if (!mod) { + mod = 0; + } + module.setModValue(action, ((1 + mod / 10000) * (1 + actionValue) - 1) * 10000); + } + } + + // Need to fix up a few items + + // Shield boosters are treated internally as straight modifiers, so rather than (for example) + // being a 4% boost they are a 104% multiplier. Unfortunately this means that our % modification + // is incorrect so we fix it + if (module.grp === 'sb' && module.getModValue('shieldboost')) { + const alteredBoost = (1 + module.shieldboost) * (module.getModValue('shieldboost')); + module.setModValue('shieldboost', alteredBoost / module.shieldboost); + } + + // Jitter is in degrees not % so need to divide it by 100 to obtain the correct number + if (module.getModValue('jitter')) { + module.setModValue('jitter', module.getModValue('jitter') / 100); + } + + // FD uses interval between bursts internally, so we need to translate this to a real rate of fire + if (module.getModValue('rof')) { + module.setModValue('rof', (1 / (1 + module.getModValue('jitter'))) - 1); + } +} +