diff --git a/ChangeLog.md b/ChangeLog.md new file mode 100644 index 00000000..777085b7 --- /dev/null +++ b/ChangeLog.md @@ -0,0 +1,20 @@ +#2.2.x + * Update DPS/HPS/EPS in real-time as modifiers change + * Use coriolis-data 2.2.2: + * Add distributor draw modifier to shield generators + * Remove modifiers for sensors + * 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 + * Ensure that retrofit tab only shows changed modules + * Fix import and export of ships with modifications, bump schema version to 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 from companion API output through import feature + * Add ability to import from companion API output through URL diff --git a/__tests__/fixtures/anaconda-test-detailed-export-v4.json b/__tests__/fixtures/anaconda-test-detailed-export-v4.json new file mode 100644 index 00000000..4bda3ad3 --- /dev/null +++ b/__tests__/fixtures/anaconda-test-detailed-export-v4.json @@ -0,0 +1,313 @@ +{ + "$schema": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#", + "name": "Test My Ship", + "ship": "Anaconda", + "references": [ + { + "name": "Coriolis.io", + "url": "http://localhost:3300/outfit/anaconda/48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship", + "old-code": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==", + "code": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==", + "shipId": "anaconda" + } + ], + "components": { + "standard": { + "bulkheads": "Reactive Surface Composite", + "cargoHatch": { + "enabled": false, + "priority": 5 + }, + "powerPlant": { + "class": 8, + "rating": "A", + "enabled": true, + "priority": 1, + "modifications": { + "pgen": 1000 + } + }, + "thrusters": { + "class": 6, + "rating": "A", + "enabled": true, + "priority": 1 + }, + "frameShiftDrive": { + "class": 6, + "rating": "A", + "enabled": true, + "priority": 3 + }, + "lifeSupport": { + "class": 5, + "rating": "A", + "enabled": true, + "priority": 1 + }, + "powerDistributor": { + "class": 8, + "rating": "A", + "enabled": true, + "priority": 1 + }, + "sensors": { + "class": 8, + "rating": "A", + "enabled": true, + "priority": 1 + }, + "fuelTank": { + "class": 5, + "rating": "C", + "enabled": true, + "priority": 1 + } + }, + "hardpoints": [ + { + "class": 4, + "rating": "A", + "enabled": true, + "priority": 2, + "group": "Plasma Accelerator", + "mount": "Fixed" + }, + { + "class": 3, + "rating": "D", + "enabled": true, + "priority": 2, + "group": "Beam Laser", + "mount": "Turret" + }, + { + "class": 3, + "rating": "D", + "enabled": true, + "priority": 2, + "group": "Beam Laser", + "mount": "Turret" + }, + { + "class": 3, + "rating": "D", + "enabled": true, + "priority": 2, + "group": "Beam Laser", + "mount": "Turret" + }, + { + "class": 2, + "rating": "E", + "enabled": true, + "priority": 2, + "group": "Cannon", + "mount": "Turret" + }, + { + "class": 2, + "rating": "E", + "enabled": true, + "priority": 2, + "group": "Cannon", + "mount": "Turret" + }, + { + "class": 1, + "rating": "F", + "enabled": true, + "priority": 2, + "group": "Beam Laser", + "mount": "Turret" + }, + { + "class": 1, + "rating": "F", + "enabled": true, + "priority": 2, + "group": "Beam Laser", + "mount": "Turret" + } + ], + "utility": [ + { + "class": 0, + "rating": "A", + "enabled": true, + "priority": 1, + "group": "Shield Booster" + }, + { + "class": 0, + "rating": "A", + "enabled": true, + "priority": 1, + "group": "Shield Booster" + }, + null, + { + "class": 0, + "rating": "C", + "enabled": true, + "priority": 2, + "group": "Kill Warrant Scanner" + }, + { + "class": 0, + "rating": "C", + "enabled": true, + "priority": 2, + "group": "Cargo Scanner" + }, + { + "class": 0, + "rating": "F", + "enabled": false, + "priority": 1, + "group": "Electronic Countermeasure", + "name": "Electronic Countermeasure" + }, + { + "class": 0, + "rating": "I", + "enabled": true, + "priority": 1, + "group": "Chaff Launcher", + "name": "Chaff Launcher" + }, + { + "class": 0, + "rating": "I", + "enabled": true, + "priority": 2, + "group": "Point Defence", + "name": "Point Defence" + } + ], + "internal": [ + { + "class": 7, + "rating": "A", + "enabled": true, + "priority": 1, + "group": "Shield Generator" + }, + { + "class": 6, + "rating": "A", + "enabled": true, + "priority": 1, + "group": "Shield Cell Bank" + }, + { + "class": 6, + "rating": "E", + "enabled": true, + "priority": 1, + "group": "Cargo Rack" + }, + { + "class": 5, + "rating": "D", + "enabled": true, + "priority": 1, + "group": "Hull Reinforcement Package" + }, + { + "class": 5, + "rating": "E", + "enabled": true, + "priority": 1, + "group": "Cargo Rack" + }, + null, + null, + { + "class": 4, + "rating": "E", + "enabled": true, + "priority": 1, + "group": "Cargo Rack" + }, + { + "class": 4, + "rating": "E", + "enabled": true, + "priority": 1, + "group": "Cargo Rack" + }, + { + "class": 4, + "rating": "A", + "enabled": true, + "priority": 3, + "group": "Fuel Scoop" + }, + { + "class": 2, + "rating": "A", + "enabled": true, + "priority": 3, + "group": "Frame Shift Drive Interdictor" + } + ] + }, + "stats": { + "class": 3, + "fighterHangars": 1, + "hullCost": 141889930, + "speed": 180, + "topSpeed": 186.5, + "boost": 240, + "boostEnergy": 27, + "topBoost": 248.62, + "topSpeed": 186.46, + "totalCost": 882362058, + "totalDpe": 127.26, + "totalDps": 97.74, + "totalEps": 22.71, + "totalHps": 677.29, + "totalExplDpe": 0, + "totalExplDps": 0, + "totalExplSDps": 0, + "totalHps": 33.28, + "totalKinDpe": 103.97, + "totalKinDps": 28.92, + "totalKinSDps": 21.23, + "totalSDps": 85.77, + "totalThermDpe": 23.29, + "totalThermDps": 68.82, + "totalThermSDps": 64.53, + "agility": 2, + "baseShieldStrength": 350, + "baseArmour": 945, + "hullExplRes": 0.78, + "hullKinRes": 0.73, + "hullMass": 400, + "hullThermRes": 1.37, + "masslock": 23, + "pipSpeed": 0.14, + "moduleCostMultiplier": 1, + "fuelCapacity": 32, + "cargoCapacity": 128, + "ladenMass": 1339.2, + "armour": 2227.5, + "baseArmour": 525, + "unladenMass": 1179.2, + "powerAvailable": 39.6, + "powerRetracted": 23.33, + "powerDeployed": 34.76, + "unladenRange": 18.49, + "fullTankRange": 18.12, + "ladenRange": 16.39, + "unladenFastestRange": 73.21, + "ladenFastestRange": 66.15, + "maxJumpCount": 4, + "shield": 833, + "shieldCells": 1840, + "shieldExplRes": 0.5, + "shieldKinRes": 0.6, + "shieldThermRes": 1.2 + } +} 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__/fixtures/ed-shipyard-import-valid.json b/__tests__/fixtures/ed-shipyard-import-valid.json index b056d143..68c62411 100644 --- a/__tests__/fixtures/ed-shipyard-import-valid.json +++ b/__tests__/fixtures/ed-shipyard-import-valid.json @@ -2,31 +2,31 @@ { "shipId": "anaconda", "buildName": "Imported Anaconda", - "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=", + "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.", "buildText": "[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n" }, { "shipId": "anaconda", "buildName": "Imported Anaconda", - "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=", + "buildCode": "0pyttFolodDsyf5------1717--------05044j-03--2h--00.Iw18ZlA=.Aw18ZlA=.", "buildText": "\n\n \t[Anaconda]\nS: 1F/F Pulse Laser\nS: 1F/F Pulse Laser\n\nBH: 1I Lightweight Alloy\nRB: 8E Power Plant\nTM: 7E Thrusters\nFH: 6E Frame Shift Drive\nEC: 5E Life Support\nPC: 8E Power Distributor\nSS: 8E Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n7: 6E Cargo Rack (Capacity: 64)\n6: 5E Cargo Rack (Capacity: 32)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 1E Basic Discovery Scanner\n2: 1E Cargo Rack (Capacity: 2)\n" }, { "shipId": "cobra_mk_iii", "buildName": "Imported Cobra Mk III", - "buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==", + "buildCode": "0patcFeldd5sdf41712222503040202490f242h.Iw1-kA==.Aw1-kA==.", "buildText": "[Cobra Mk III]\nM: 1F/F Pulse Laser\nM: 1G/G Burst Laser\nS: 1E/T Fragment Cannon\nS: 1G/T Multi-cannon\nU: 0I Point Defence\nU: 0A Shield Booster\n\nBH: 1I Lightweight Alloy\nRB: 4A Power Plant\nTM: 4C Thrusters\nFH: 4E Frame Shift Drive\nEC: 3D Life Support\nPC: 2A Power Distributor\nSS: 3D Sensors\nFS: 4C Fuel Tank (Capacity: 16)\n\n4: 3E Cargo Rack (Capacity: 8)\n4: 3E Cargo Rack (Capacity: 8)\n4: 4E Shield Generator\n2: 2C Auto Field-Maintenance Unit\n2: 1E Standard Docking Computer\n2: 1E Basic Discovery Scanner\n---\nShield: 112.29 MJ\nPower : 10.45 MW retracted (67%)\n 12.16 MW deployed (78%)\n 15.60 MW available\nCargo : 16 T\nFuel : 16 T\nMass : 235.5 T empty\n 267.5 T full\nRange : 10.69 LY unladen\n 10.05 LY laden\nPrice : 2,929,040 CR\nRe-Buy: 146,452 CR @ 95% insurance\n" }, { "shipId": "type_9_heavy", "buildName": "Imported Type-9 Heavy", - "buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==", + "buildCode": "3pftsFklkdisif57e2k2f2h110001020306054j03022f01242i.Iw18eQ==.Aw18eQ==.", "buildText": "[Type-9 Heavy]\nM: 2D/G Fragment Cannon\nM: 2I/F Mine Launcher\nM: 2B/FD Missile Rack\nS: 1I/FS Torpedo Pylon\nS: 1F/F Burst Laser\nU: 0I Chaff Launcher\nU: 0F Electronic Countermeasure\nU: 0I Heat Sink Launcher\nU: 0I Point Defence\n\nBH: 1I Mirrored Surface Composite\nRB: 5A Power Plant\nTM: 7D Thrusters\nFH: 6A Frame Shift Drive\nEC: 5A Life Support\nPC: 4D Power Distributor\nSS: 4D Sensors\nFS: 5C Fuel Tank (Capacity: 32)\n\n8: 7E Cargo Rack (Capacity: 128)\n7: 6E Cargo Rack (Capacity: 64)\n6: 6E Shield Generator\n5: 4E Cargo Rack (Capacity: 16)\n4: 3E Cargo Rack (Capacity: 8)\n4: 1C Advanced Discovery Scanner\n3: 2E Cargo Rack (Capacity: 4)\n3: 1E Standard Docking Computer\n2: 1C Detailed Surface Scanner\n" }, { "shipId": "vulture", "buildName": "Imported Vulture", - "buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==", + "buildCode": "4patfFalddksif31e1e0e0j04044a0n532jf1.Iw19kA==.Aw19kA==.", "buildText": "[Vulture]\nL: 3E/G Pulse Laser\nL: 3E/G Pulse Laser\nU: 0A Frame Shift Wake Scanner\nU: 0A Kill Warrant Scanner\nU: 0A Shield Booster\nU: 0A Shield Booster\n\nBH: 1I Reactive Surface Composite\nRB: 4A Power Plant\nTM: 5A Thrusters\nFH: 4A Frame Shift Drive\nEC: 3D Life Support\nPC: 5A Power Distributor\nSS: 4D Sensors\nFS: 3C Fuel Tank (Capacity: 8)\n\n5: 5A Shield Generator\n4: 4A Auto Field-Maintenance Unit\n2: 2A Shield Cell Bank\n1: 1A Fuel Scoop\n1: 1C Fuel Tank (Capacity: 2)" } -] \ No newline at end of file +] diff --git a/__tests__/fixtures/expected-builds.json b/__tests__/fixtures/expected-builds.json index d7f12000..2b985df4 100644 --- a/__tests__/fixtures/expected-builds.json +++ b/__tests__/fixtures/expected-builds.json @@ -1,50 +1,50 @@ { "type_6_transporter": { - "Cargo": "0p0tdFal8d8s8f4-----04040303430101.Iw1-kA==.Aw1-kA==", - "Miner": "0p5tdFal8d8s8f42l2l---040403451q0101.Iw1-kA==.Aw1-kA==", - "Hopper": "0p0tdFal8d0s8f41717---030302024300-.Iw1-kA==.Aw1-kA==" + "Cargo": "0p0tdFal8d8s8f4-----04040303430101.Iw1-kA==.Aw1-kA==.", + "Miner": "0p5tdFal8d8s8f42l2l---040403451q0101.Iw1-kA==.Aw1-kA==.", + "Hopper": "0p0tdFal8d0s8f41717---030302024300-.Iw1-kA==.Aw1-kA==." }, "type_7_transport": { - "Cargo": "0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==", - "Miner": "0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==" + "Cargo": "0p0tiFfliddsdf5--------0505040403480101.Iw18aQ==.Aw18aQ==.", + "Miner": "0pdtiFflid8sdf5--2l2l----0505041v03450000.Iw18aQ==.Aw18aQ==." }, "federal_dropship": { - "Cargo": "0pdtiFflnddsif4-1717------05040448020201.Iw18aQ==.Aw18aQ==" + "Cargo": "0pdtiFflnddsif4-1717------05040448020201.Iw18aQ==.Aw18aQ==." }, "asp": { - "Miner": "2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==" + "Miner": "2pftfFflidfskf50s0s24242l2l---04054a1q02022o27.Iw18WQ==.Aw18WQ==." }, "imperial_clipper": { - "Cargo": "0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==", - "Dream": "2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.Iw18aQ==.Aw18aQ==", - "Current": "0patkFflndfskf4----------------.Iw18aQ==.Aw18aQ==" + "Cargo": "0p5tiFflndisnf4--0s0s----0605450302020101.Iw18aQ==.Aw18aQ==.", + "Dream": "2pktkFflndpskf40v0v0s0s0404040n4k5n5d2b29292o-.Iw18aQ==.Aw18aQ==.", + "Current": "0patkFflndfskf4----------------.Iw18aQ==.Aw18aQ==." }, "type_9_heavy": { - "Current": "0patsFklndnsif6---------0706054a0303020224.Iw18eQ==.Aw18eQ==" + "Current": "0patsFklndnsif6---------0706054a0303020224.Iw18eQ==.Aw18eQ==." }, "python": { - "Cargo": "0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==", - "Miner": "0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.Aw18eQ==", - "Dream": "2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw18eQ==.Aw18eQ==", - "Missile": "0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ==" + "Cargo": "0patnFflidsssf5---------050505040448020201.Iw18eQ==.Aw18eQ==.", + "Miner": "0pktkFflidpspf50v0v0v2m2m0404--050505Ce4a1v02022o.Iw18eQ==.Aw18eQ==.", + "Dream": "2pptkFfliduspf50v0v0v27270404040m5n5n4f2d2d032t0201.Iw18eQ==.Aw18eQ==.", + "Missile": "0pttoFjljdystf52f2g2d2ePh----04044j03---002h.Iw18eQ==.Aw18eQ==." }, "anaconda": { - "Dream": "4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.Iw18ZlA=.Aw18ZlA=", - "Cargo": "0patnFklndnsxf5----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=", - "Current": "0patnFklndksxf5----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=", - "Explorer": "0patnFklndksxf5--------0202------f7050505040s372f2i4524.Iw18ZlA=.Aw18ZlA=", - "Test": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18ZlA=.Aw18ZlA=" + "Dream": "4putpFklndzsuf52c0o0o0o1m1m0q0q0404040l0b0100004k5n5n112d2d040303326b.Iw18ZlA=.Aw18ZlA=.", + "Cargo": "0patnFklndnsxf5----------------0605050504040445030301.Iw18ZlA=.Aw18ZlA=.", + "Current": "0patnFklndksxf5----------------0605050504040403034524.Iw18ZlA=.Aw18ZlA=.", + "Explorer": "0patnFklndksxf5--------0202------f7050505040s372f2i4524.Iw18ZlA=.Aw18ZlA=.", + "Test": "4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.Iw18ZlA=.Aw18ZlA=." }, "diamondback_explorer": { - "Explorer": "0p0tdFfldddsdf5---0202--320p432i2f.Iw1-kA==.Aw1-kA==" + "Explorer": "0p0tdFfldddsdf5---0202--320p432i2f.Iw1-kA==.Aw1-kA==." }, "vulture": { - "Bounty Hunter": "3patcFalddksff31e1e0404-0l4a5d27662j.Iw19kA==.Aw19kA==" + "Bounty Hunter": "3patcFalddksff31e1e0404-0l4a5d27662j.Iw19kA==.Aw19kA==." }, "fer_de_lance": { - "Attack": "2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.Aw18aQ==" + "Attack": "2pfthFalidpsff31r0s0s0s0s000404-04-4a-5d27-.Iw18aQ==.Aw18aQ==." }, "eagle": { - "Figther": "4p0t5F5l3d5s5f20p0p24-40532j-.Iw1-EA==.Aw1-EA==" + "Figther": "4p0t5F5l3d5s5f20p0p24-40532j-.Iw1-EA==.Aw1-EA==." } -} \ No newline at end of file +} diff --git a/__tests__/fixtures/valid-backup.json b/__tests__/fixtures/valid-backup.json index 683511e1..a705dd3a 100644 --- a/__tests__/fixtures/valid-backup.json +++ b/__tests__/fixtures/valid-backup.json @@ -33,7 +33,8 @@ }, "anaconda": { "Dream": "48A6A6A5A8A8A5C2c0o0o0o1m1m0q0q0404040l0b0100034k5n05050404040303326b.AwRj4yo5dig=.MwBhEYy6duwEziA=", - "Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA=" + "Cargo": "03A7D6A5D4D8D5C----------------060505054d040403030301.AwRj4yuqg===.Aw18ZlA=", + "Modified": "0pyttFolodDsyf5------1717--------05044j-03----2h00.Iw18ZlA=.Aw18ZlA=.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==" }, "diamondback_explorer": { "Explorer": "02A4D5A3D3D3D5C-------320p432i2f.AwRj4zTI.AwiMIypI" @@ -63,4 +64,4 @@ 1, 1 ] -} \ No newline at end of file +} diff --git a/__tests__/test-import.js b/__tests__/test-import.js index 27aa9a4c..fd5ef1b1 100644 --- a/__tests__/test-import.js +++ b/__tests__/test-import.js @@ -129,7 +129,7 @@ describe('Import Modal', function() { }); }); - describe('Import Detailed Build', function() { + describe('Import Detailed V3 Build', function() { beforeEach(reset); @@ -142,7 +142,7 @@ describe('Import Modal', function() { expect(modal.state.singleBuild).toBe(true); clickProceed(); expect(MockRouter.go.mock.calls.length).toBe(1); - expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA?bn=Test%20My%20Ship'); + expect(MockRouter.go.mock.calls[0][0]).toBe('/outfit/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.?bn=Test%20My%20Ship'); }); it('catches an invalid build', function() { @@ -154,6 +154,23 @@ describe('Import Modal', function() { }); }); + describe('Import Detailed V4 Build', function() { + + beforeEach(reset); + + it('imports a valid v4 build', function() { + const importData = require('./fixtures/anaconda-test-detailed-export-v4'); + 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/anaconda/4putkFklkdzsuf52c0o0o0o1m1m0q0q0404-0l0b0100034k5n052d04--0303326b.AwRj4zNKqA==.CwBhCYzBGW9qCTSqs5xA.H4sIAAAAAAAAA2MUe8HMwPD-PwDDhxeuCAAAAA==?bn=Test%20My%20Ship'); + }); + }); + describe('Import Detaild Builds Array', function() { beforeEach(reset); @@ -179,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/2putsFklndzsxf50x0x7l28281919040404040402020l06p05sf63c5ifhv66g2f.AwRj4zNaKA==.CwRgDBldUExuBiQqA===.H4sIAAAAAAAAAx2Rzy4DURTGz7TuzHRu47ZjWreKlg5iQ9KFZ9CENyBWtWo8gIUFsamteAIJi0qEWIhdN11ZEN1IwyNYVKRpcXzH5su553f_XyfvKiLTYma-TkScyHVcokoYEdmbBNDsiDla-WUOT5LgyfAshHdvyGyjFFHUQCSrBU8TLT4gYq4DNL_LhNTFN3PwiqdZQyX2C-sekep-Mrs1RIbnDppsIogD1UAtN7JEM9eIzZg8hmhsEU32gFmrdgB_UARvjYEr4QMUMffoxGnV-M8X3hZ_lAO-gmWq2Eq2IVtDOzZ2Hbbuws6KxCKmKUUydgRb3woSiUXMs6Cs7Qt6FCQSi5hxkNKhj6qhfcPU_kU4wYrFMseSOmFXMKbuwZsViUWMlq1sbhvJ_lKyfqTqEJGJyoC5eIpU9x2TRnUswYXyF77BW4Z3qQuv05GDTpfvcDzvSbxJ5DtV_aHS1I4clyB2A5_b-pAL8x_enn626gEAAA==?bn=Imported%20Federal%20Corvette'); + }); + }); + describe('Import E:D Shipyard Builds', function() { it('imports a valid builds', function() { diff --git a/__tests__/test-serializer.js b/__tests__/test-serializer.js index c03aff75..59260558 100644 --- a/__tests__/test-serializer.js +++ b/__tests__/test-serializer.js @@ -4,16 +4,16 @@ import * as Serializer from '../src/app/shipyard/Serializer'; import jsen from 'jsen'; describe("Serializer", function() { - const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v3'); + const anacondaTestExport = require.requireActual('./fixtures/anaconda-test-detailed-export-v4'); const code = anacondaTestExport.references[0].code; const anaconda = Ships.anaconda; - const validate = jsen(require('../src/schemas/ship-loadout/3')); + const validate = jsen(require('../src/schemas/ship-loadout/4')); describe("To Detailed Build", function() { let testBuild = new Ship('anaconda', anaconda.properties, anaconda.slots).buildFrom(code); let exportData = Serializer.toDetailedBuild('Test My Ship', testBuild); - it("conforms to the v3 ship-loadout schema", function() { + it("conforms to the v4 ship-loadout schema", function() { expect(validate(exportData)).toBe(true); }); @@ -31,7 +31,7 @@ describe("Serializer", function() { const builds = require('./fixtures/expected-builds'); const exportData = Serializer.toDetailedExport(builds); - it("conforms to the v3 ship-loadout schema", function() { + it("conforms to the v4 ship-loadout schema", function() { expect(exportData instanceof Array).toBe(true); for (let detailedBuild of exportData) { diff --git a/__tests__/test-ship.js b/__tests__/test-ship.js index 00ef39cc..9d52f01c 100644 --- a/__tests__/test-ship.js +++ b/__tests__/test-ship.js @@ -24,7 +24,7 @@ describe("Ship", function() { expect(ship.fuelCapacity).toBeGreaterThan(0, s + ' fuelCapacity'); expect(ship.unladenFastestRange).toBeGreaterThan(0, s + ' unladenFastestRange'); expect(ship.ladenFastestRange).toBeGreaterThan(0, s + ' ladenFastestRange'); - expect(ship.shieldStrength).toBeGreaterThan(0, s + ' shieldStrength'); + expect(ship.shield).toBeGreaterThan(0, s + ' shield'); expect(ship.armour).toBeGreaterThan(0, s + ' armour'); expect(ship.topSpeed).toBeGreaterThan(0, s + ' topSpeed'); } diff --git a/package.json b/package.json index 674af442..113483eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "coriolis_shipyard", - "version": "2.2.1", + "version": "2.2.2", "repository": { "type": "git", "url": "https://github.com/EDCD/coriolis" @@ -84,12 +84,13 @@ "dependencies": { "babel-polyfill": "*", "classnames": "^2.2.0", + "browserify-zlib": "ipfs/browserify-zlib", "coriolis-data": "EDCD/coriolis-data", "d3": "3.5.16", "fbemitter": "^2.0.0", "lodash": "^4.15.0", "lz-string": "^1.4.4", - "react-number-editor": "^4.0.2", + "react-number-editor": "Athanasius/react-number-editor.git#miggy", "react": "^15.0.1", "react-dom": "^15.0.1", "superagent": "^1.4.0" diff --git a/src/app/Coriolis.jsx b/src/app/Coriolis.jsx index ad17d1d5..2e180587 100644 --- a/src/app/Coriolis.jsx +++ b/src/app/Coriolis.jsx @@ -7,6 +7,8 @@ import Persist from './stores/Persist'; import Header from './components/Header'; import Tooltip from './components/Tooltip'; import ModalImport from './components/ModalImport'; +import * as CompanionApiUtils from './utils/CompanionApiUtils'; +import { outfitURL } from './utils/UrlGenerators' import AboutPage from './pages/AboutPage'; import NotFoundPage from './pages/NotFoundPage'; @@ -15,6 +17,8 @@ import ComparisonPage from './pages/ComparisonPage'; import ShipyardPage from './pages/ShipyardPage'; import ErrorDetails from './pages/ErrorDetails'; +const zlib = require('zlib'); + /** * Coriolis App */ @@ -52,6 +56,7 @@ export default class Coriolis extends React.Component { this._onLanguageChange = this._onLanguageChange.bind(this); this._onSizeRatioChange = this._onSizeRatioChange.bind(this); this._keyDown = this._keyDown.bind(this); + this._importBuild = this._importBuild.bind(this); this.emitter = new EventEmitter(); this.state = { @@ -63,13 +68,36 @@ export default class Coriolis extends React.Component { }; Router('', (r) => this._setPage(ShipyardPage, r)); + Router('/import?', (r) => this._importBuild(r)); + Router('/import/:data', (r) => this._importBuild(r)); + Router('/outfit/?', (r) => this._setPage(OutfittingPage, r)); + Router('/outfit/:ship/?', (r) => this._setPage(OutfittingPage, r)); Router('/outfit/:ship/:code?', (r) => this._setPage(OutfittingPage, r)); Router('/compare/:name?', (r) => this._setPage(ComparisonPage, r)); + Router('/comparison?', (r) => this._setPage(ComparisonPage, r)); Router('/comparison/:code', (r) => this._setPage(ComparisonPage, r)); Router('/about', (r) => this._setPage(AboutPage, r)); Router('*', (r) => this._setPage(null, r)); } + /** + * Import a build directly + * @param {Object} r The current route + */ + _importBuild(r) { + try { + // Need to decode and gunzip the data, then build the ship + const data = zlib.gunzipSync(new Buffer(r.params.data, 'base64')); + const json = JSON.parse(data); + const ship = CompanionApiUtils.shipFromJson(json); + r.params.ship = ship.id; + r.params.code = ship.toString(); + this._setPage(OutfittingPage, r); + } catch (err) { + this._onError('Failed to import ship', r.path, 0, 0, err); + } + } + /** * Updates / Sets the page and route context * @param {[type]} page The page to be shown diff --git a/src/app/components/ComparisonTable.jsx b/src/app/components/ComparisonTable.jsx index ca99d307..3f831a3a 100644 --- a/src/app/components/ComparisonTable.jsx +++ b/src/app/components/ComparisonTable.jsx @@ -2,6 +2,7 @@ import React from 'react'; import TranslatedComponent from './TranslatedComponent'; import Link from './Link'; import cn from 'classnames'; +import { outfitURL } from '../utils/UrlGenerators'; import { SizeMap } from '../shipyard/Constants'; @@ -71,7 +72,7 @@ export default class ComparisonTable extends TranslatedComponent { * @return {React.Component} Table row */ _buildRow(build, facets, formats, units) { - let url = `/outfit/${build.id}/${build.toString()}?bn=${build.buildName}`; + let url = outfitURL(build.id, build.toString(), build.buildName) let cells = [ {build.name}, {build.buildName} diff --git a/src/app/components/CostSection.jsx b/src/app/components/CostSection.jsx index 699f5d20..2df44855 100644 --- a/src/app/components/CostSection.jsx +++ b/src/app/components/CostSection.jsx @@ -379,7 +379,7 @@ export default class CostSection extends TranslatedComponent { {translate('retrofit from')} - {options} @@ -419,7 +419,9 @@ export default class CostSection extends TranslatedComponent { let retroSlotGroup = retrofitShip[g]; let slotGroup = ship[g]; for (i = 0, l = slotGroup.length; i < l; i++) { - if (slotGroup[i].m != retroSlotGroup[i].m) { + const modId = slotGroup[i].m ? slotGroup[i].m.eddbID : null; + const retroModId = retroSlotGroup[i].m ? retroSlotGroup[i].m.eddbID : null; + if (modId != retroModId) { item = { netCost: 0, retroItem: retroSlotGroup[i] }; if (slotGroup[i].m) { item.buyName = slotGroup[i].m.name || slotGroup[i].m.grp; @@ -505,19 +507,19 @@ export default class CostSection extends TranslatedComponent { scoop = true; break; case 'scb': - q = slotGroup[i].m.cells; + q = slotGroup[i].m.getCells(); break; case 'am': - q = slotGroup[i].m.ammo; + q = slotGroup[i].m.getAmmo(); break; case 'pv': - srvs += slotGroup[i].m.vehicles; + srvs += slotGroup[i].m.getBays(); break; case 'fx': case 'hb': case 'cc': case 'pc': limpets = ship.cargoCapacity; break; default: - q = slotGroup[i].m.clip + slotGroup[i].m.ammo; + q = slotGroup[i].m.getClip() + slotGroup[i].m.getAmmo(); } // Calculate ammo costs only if a cost is specified if (slotGroup[i].m.ammocost > 0) { @@ -530,6 +532,17 @@ export default class CostSection extends TranslatedComponent { ammoCosts.push(item); ammoTotal += item.total; } + // Add fighters + if (slotGroup[i].m.grp === 'fh') { + item = { + m: slotGroup[i].m, + max: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays(), + cost: slotGroup[i].m.fightercost, + total: slotGroup[i].m.getRebuildsPerBay() * slotGroup[i].m.getBays() * slotGroup[i].m.fightercost + }; + ammoCosts.push(item); + ammoTotal += item.total; + } } } } @@ -550,12 +563,13 @@ export default class CostSection extends TranslatedComponent { item = { m: { name: 'SRVs', class: '', rating: '' }, max: srvs, - cost: 6005, - total: srvs * 6005 + cost: 1030, + total: srvs * 1030 }; ammoCosts.push(item); ammoTotal += item.total; } + // Calculate refuel costs if no scoop present if (!scoop) { item = { diff --git a/src/app/components/DefenceSummary.jsx b/src/app/components/DefenceSummary.jsx new file mode 100644 index 00000000..d9ec54a9 --- /dev/null +++ b/src/app/components/DefenceSummary.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; +import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; + +/** + * Defence summary + */ +export default class DefenceSummary extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render defence summary + * @return {React.Component} contents + */ + render() { + let ship = this.props.ship; + let { language, tooltip, termtip } = this.context; + let { formats, translate, units } = language; + let hide = tooltip.bind(null, null); + + return ( + +

{translate('defence summary')}

+ + + {ship.shield ? + + + : null } + {ship.shield ? + + + + + + : null } + {ship.shield ? + + + + + + : null } + + { ship.shield && ship.shieldCells ? + + + : null } + + + + + + + + + + + +

{translate('shields')}: {formats.int(ship.shield)} {units.MJ}

{translate('recovery')}{formats.time(ship.calcShieldRecovery())}{translate('recharge')}{formats.time(ship.calcShieldRecharge())}
{translate('damage from')} {formats.pct1(ship.shieldExplRes || 1)} {formats.pct1(ship.shieldKinRes || 1)} {formats.pct1(ship.shieldThermRes || 1)}

{translate('shield cells')}: {formats.int(ship.shieldCells)} {units.MJ}

{translate('armour')}: {formats.int(ship.armour)}

{translate('damage from')} {formats.pct1(ship.hullExplRes || 1)} {formats.pct1(ship.hullKinRes || 1)} {formats.pct1(ship.hullThermRes || 1)}
+
+ ); + } +} diff --git a/src/app/components/HardpointSlot.jsx b/src/app/components/HardpointSlot.jsx index 9ecc6dae..fa3e29e5 100644 --- a/src/app/components/HardpointSlot.jsx +++ b/src/app/components/HardpointSlot.jsx @@ -1,6 +1,7 @@ import React from 'react'; import Slot from './Slot'; -import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications } from './SvgIcons'; +import Persist from '../stores/Persist'; +import { DamageKinetic, DamageThermal, DamageExplosive, MountFixed, MountGimballed, MountTurret, ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -41,6 +42,7 @@ export default class HardpointSlot extends Slot { let { drag, drop } = this.props; let { termtip, tooltip } = this.context; let validMods = Modifications.validity[m.grp] || []; + let showModuleResistances = Persist.showModuleResistances(); return
@@ -51,7 +53,9 @@ export default class HardpointSlot extends Slot { {m.type && m.type.match('K') ? : ''} {m.type && m.type.match('T') ? : ''} {m.type && m.type.match('E') ? : ''} - {classRating} {translate(m.name || m.grp)}
+ {classRating} {translate(m.name || m.grp)}{ m.mods && Object.keys(m.mods).length > 0 ? : null } +
+
{formats.round(m.getMass())}{u.T}
@@ -60,10 +64,15 @@ export default class HardpointSlot extends Slot { { m.getHps() ?
{translate('HPS')}: {formats.round1(m.getHps())} { m.getClip() ? ({formats.round1((m.getClip() * m.getHps() / m.getRoF()) / ((m.getClip() / m.getRoF()) + m.getReload())) }) : null }
: null } { m.getDps() && m.getEps() ?
{translate('DPE')}: {formats.f1(m.getDps() / m.getEps())}
: null } { m.getRoF() ?
{translate('ROF')}: {formats.f1(m.getRoF())}{u.ps}
: null } - { m.getRange() && !m.getDps() ?
{translate('Range')} : {formats.round(m.getRange() / 1000)}{u.km}
: null } + { m.getRange() ?
{translate('range')} {formats.f1(m.getRange() / 1000)}{u.km}
: null } { m.getShieldBoost() ?
+{formats.pct1(m.getShieldBoost())}
: null } { m.getAmmo() ?
{translate('ammunition')}: {formats.int(m.getClip())}/{formats.int(m.getAmmo())}
: null } + { m.getJitter() ?
{translate('jitter')}: {formats.f2(m.getJitter())}°
: null } + { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null } + { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } + { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } { m && validMods.length > 0 ?
: null } +
; } else { diff --git a/src/app/components/Header.jsx b/src/app/components/Header.jsx index db2294d5..8b679a6b 100644 --- a/src/app/components/Header.jsx +++ b/src/app/components/Header.jsx @@ -203,6 +203,13 @@ export default class Header extends TranslatedComponent { Persist.showTooltips(!Persist.showTooltips()); } + /** + * Toggle module resistances setting + */ + _toggleModuleResistances() { + Persist.showModuleResistances(!Persist.showModuleResistances()); + } + /** * Show delete all modal * @param {SyntheticEvent} e Event @@ -359,6 +366,7 @@ export default class Header extends TranslatedComponent { _getSettingsMenu() { let translate = this.context.language.translate; let tips = Persist.showTooltips(); + let moduleResistances = Persist.showModuleResistances(); return (
e.stopPropagation() }> @@ -376,6 +384,10 @@ export default class Header extends TranslatedComponent { {translate('tooltips')} {(tips ? '✓' : '✗')} + + {translate('module resistances')} + {(moduleResistances ? '✓' : '✗')} + {translate('insurance')} @@ -438,6 +450,7 @@ export default class Header extends TranslatedComponent { Persist.addListener('deletedAll', update); Persist.addListener('builds', update); Persist.addListener('tooltips', update); + Persist.addListener('moduleresistances', update); } /** diff --git a/src/app/components/InternalSlot.jsx b/src/app/components/InternalSlot.jsx index 318bb85b..f936b6be 100644 --- a/src/app/components/InternalSlot.jsx +++ b/src/app/components/InternalSlot.jsx @@ -1,6 +1,7 @@ import React from 'react'; import Slot from './Slot'; -import { ListModifications } from './SvgIcons'; +import Persist from '../stores/Persist'; +import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -23,11 +24,12 @@ export default class InternalSlot extends Slot { let { drag, drop, ship } = this.props; let { termtip, tooltip } = this.context; let validMods = Modifications.validity[m.grp] || []; + let showModuleResistances = Persist.showModuleResistances(); let mass = m.getMass() || m.cargo || m.fuel || 0; return
-
{classRating} {translate(m.name || m.grp)}
+
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : ''}
{formats.round(mass)}{u.T}
@@ -38,7 +40,7 @@ export default class InternalSlot extends Slot { { m.rate ?
{translate('rate')}: {m.rate}{u.kgs}   {translate('refuel time')}: {formats.time(this.props.fuel * 1000 / m.rate)}
: null } { m.getAmmo() ?
{translate('ammunition')}: {formats.gen(m.getAmmo())}
: null } { m.cells ?
{translate('cells')}: {m.cells}
: null } - { m.recharge ?
{translate('recharge')}: {m.recharge} MJ   {translate('total')}: {m.cells * m.recharge}{u.MJ}
: null } + { m.shieldreinforcement ?
{translate('shieldreinforcement')}: {formats.int(m.getShieldReinforcement())} MJ   {translate('total')}: {formats.int(m.cells * m.getShieldReinforcement())}{u.MJ}
: null } { m.repair ?
{translate('repair')}: {m.repair}
: null } { m.getFacingLimit() ?
{translate('facinglimit')} {formats.f1(m.getFacingLimit())}°
: null } { m.getRange() ?
{translate('range')} {formats.f2(m.getRange())}{u.km}
: null } @@ -49,8 +51,12 @@ export default class InternalSlot extends Slot { { m.rangeLS ?
{translate('range')}: {m.rangeLS}{u.Ls}
: null } { m.rangeLS === null ?
∞{u.Ls}
: null } { m.rangeRating ?
{translate('range')}: {m.rangeRating}
: null } - { m.getHullReinforcement() ?
+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost'))} {translate('armour')}
: null } + { m.getHullReinforcement() ?
+{formats.int(m.getHullReinforcement() + ship.baseArmour * m.getModValue('hullboost') / 10000)} {translate('armour')}
: null } { m.passengers ?
{translate('passengers')}: {m.passengers}
: null } + { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null } + { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } + { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } + { m && validMods.length > 0 ?
: null }
diff --git a/src/app/components/ModalImport.jsx b/src/app/components/ModalImport.jsx index 04dd2b87..b698a9d9 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.modules != null && importData.modules.Armour != null) { // Only the companion API has this information + this._importCompanionApiBuild(importData); // Single sihp definition + } else if (importData.ship != null && importData.ship.modules != null && importData.ship.modules.Armour != 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/components/Modification.jsx b/src/app/components/Modification.jsx index c73d0607..6a1464d4 100644 --- a/src/app/components/Modification.jsx +++ b/src/app/components/Modification.jsx @@ -7,7 +7,7 @@ import NumberEditor from 'react-number-editor'; /** * Modification */ -export default class ModificationsMenu extends TranslatedComponent { +export default class Modification extends TranslatedComponent { static propTypes = { ship: React.PropTypes.object.isRequired, @@ -24,26 +24,30 @@ export default class ModificationsMenu extends TranslatedComponent { constructor(props, context) { super(props); this.state = {}; - this.state.value = this.props.m.getModValue(this.props.name) * 100 || 0; + this.state.value = this.props.m.getModValue(this.props.name) / 100 || 0; } /** * Update modification given a value. - * @param {Number} value The value to set + * @param {Number} value The value to set. This comes in as a string and must be stored in state as a string, + * because it needs to allow illegal 'numbers' ('-', '1.', etc) when the user is typing + * in a value by hand */ _updateValue(value) { - let scaledValue = Math.floor(Number(value) * 100) / 10000; + const name = this.props.name; + + let scaledValue = Math.round(Number(value) * 100); // Limit to +1000% / -100% - if (scaledValue > 10) { - scaledValue = 10; + if (scaledValue > 100000) { + scaledValue = 100000; value = 1000; } - if (scaledValue < -1) { - scaledValue = -1; + if (scaledValue < -10000) { + scaledValue = -10000; value = -100; } + let m = this.props.m; - let name = this.props.name; let ship = this.props.ship; ship.setModification(m, name, scaledValue); @@ -62,7 +66,7 @@ export default class ModificationsMenu extends TranslatedComponent { return (
{translate(name)}{name === 'jitter' ? ' (°)' : ' (%)'}
- +
); } diff --git a/src/app/components/ModificationsMenu.jsx b/src/app/components/ModificationsMenu.jsx index bdf633d3..f38c8b1e 100644 --- a/src/app/components/ModificationsMenu.jsx +++ b/src/app/components/ModificationsMenu.jsx @@ -50,11 +50,13 @@ export default class ModificationsMenu extends TranslatedComponent { * @return {React.Component} List */ render() { + let { tooltip, termtip } = this.context; return (
e.stopPropagation() } onContextMenu={stopCtxPropagation} + onMouseOver={termtip.bind(null, 'HELP_MODIFICATIONS_MENU')} onMouseOut={tooltip.bind(null, null)} > {this.state.list}
diff --git a/src/app/components/OffenceSummary.jsx b/src/app/components/OffenceSummary.jsx new file mode 100644 index 00000000..9123ce5b --- /dev/null +++ b/src/app/components/OffenceSummary.jsx @@ -0,0 +1,70 @@ +import React from 'react'; +import cn from 'classnames'; +import TranslatedComponent from './TranslatedComponent'; +import { DamageKinetic, DamageThermal, DamageExplosive } from './SvgIcons'; + +/** + * Offence summary + */ +export default class OffenceSummary extends TranslatedComponent { + static PropTypes = { + ship: React.PropTypes.object.isRequired + }; + + /** + * Constructor + * @param {Object} props React Component properties + */ + constructor(props) { + super(props); + } + + /** + * Render offence summary + * @return {React.Component} contents + */ + render() { + let ship = this.props.ship; + let { language, tooltip, termtip } = this.context; + let { formats, translate } = language; + + return ( + +

{translate('offence summary')}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{translate('dps')}: {formats.f1(ship.totalDps)}

{translate('damage by')} {formats.f1(ship.totalExplDps)} {formats.f1(ship.totalKinDps)} {formats.f1(ship.totalThermDps)}

{translate('sdps')}: {formats.f1(ship.totalSDps)}

{translate('damage by')} {formats.f1(ship.totalExplSDps)} {formats.f1(ship.totalKinSDps)} {formats.f1(ship.totalThermSDps)}

{translate('dpe')}: {formats.f1(ship.totalDpe)}

{translate('damage by')} {formats.f1(ship.totalExplDpe)} {formats.f1(ship.totalKinDpe)} {formats.f1(ship.totalThermDpe)}
+
+ ); + } +} diff --git a/src/app/components/ShipSummaryTable.jsx b/src/app/components/ShipSummaryTable.jsx index e5069369..9ab15a9d 100644 --- a/src/app/components/ShipSummaryTable.jsx +++ b/src/app/components/ShipSummaryTable.jsx @@ -34,20 +34,6 @@ export default class ShipSummaryTable extends TranslatedComponent { sgRecharge = time(ship.calcShieldRecharge()); } - // {translate('shield resistance')} - // {translate('hull resistance')} - // {translate('explosive')} - // {translate('kinetic')} - // {translate('thermal')} - // {translate('explosive')} - // {translate('kinetic')} - // {translate('thermal')} - // {pct(ship.shieldExplRes)} - // {pct(ship.shieldKinRes)} - // {pct(ship.shieldThermRes)} - // {pct(ship.hullExplRes)} - // {pct(ship.hullKinRes)} - // {pct(ship.hullThermRes)} return
@@ -58,7 +44,7 @@ export default class ShipSummaryTable extends TranslatedComponent { - + @@ -67,9 +53,6 @@ export default class ShipSummaryTable extends TranslatedComponent { - - - @@ -90,8 +73,6 @@ export default class ShipSummaryTable extends TranslatedComponent { - - diff --git a/src/app/components/StandardSlot.jsx b/src/app/components/StandardSlot.jsx index 8140f7f0..f4a61415 100644 --- a/src/app/components/StandardSlot.jsx +++ b/src/app/components/StandardSlot.jsx @@ -1,11 +1,12 @@ import React from 'react'; import cn from 'classnames'; +import Persist from '../stores/Persist'; import TranslatedComponent from './TranslatedComponent'; import { jumpRange } from '../shipyard/Calculations'; import { diffDetails } from '../utils/SlotFunctions'; import AvailableModulesMenu from './AvailableModulesMenu'; import ModificationsMenu from './ModificationsMenu'; -import { ListModifications } from './SvgIcons'; +import { ListModifications, Modified } from './SvgIcons'; import { Modifications } from 'coriolis-data/dist'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -46,6 +47,7 @@ export default class StandardSlot extends TranslatedComponent { let classRating = m.class + m.rating; let menu; let validMods = m == null ? [] : (Modifications.validity[m.grp] || []); + let showModuleResistances = Persist.showModuleResistances(); let mass = m.getMass() || m.cargo || m.fuel || 0; if (!selected) { @@ -79,11 +81,10 @@ export default class StandardSlot extends TranslatedComponent {
{slot.maxClass}
-
{classRating} {translate(m.grp == 'bh' ? m.grp : m.name || m.grp)}
+
{classRating} {translate(m.name || m.grp)}{m.mods && Object.keys(m.mods).length > 0 ? : null }
{formats.round(mass)}{units.T}
- { m.grp == 'bh' && m.name ?
{translate(m.name)}
: null } { m.getOptimalMass() ?
{translate('optimal mass')}: {formats.int(m.getOptimalMass())}{units.T}
: null } { m.getMaxMass() ?
{translate('max mass')}: {formats.int(m.getMaxMass())}{units.T}
: null } { m.getRange() ?
{translate('range')}: {formats.f2(m.getRange())}{units.km}
: null } @@ -94,6 +95,9 @@ export default class StandardSlot extends TranslatedComponent { { m.getWeaponsCapacity() ?
{translate('WEP')}: {formats.f1(m.getWeaponsCapacity())}{units.MJ} / {formats.f1(m.getWeaponsRechargeRate())}{units.MW}
: null } { m.getSystemsCapacity() ?
{translate('SYS')}: {formats.f1(m.getSystemsCapacity())}{units.MJ} / {formats.f1(m.getSystemsRechargeRate())}{units.MW}
: null } { m.getEnginesCapacity() ?
{translate('ENG')}: {formats.f1(m.getEnginesCapacity())}{units.MJ} / {formats.f1(m.getEnginesRechargeRate())}{units.MW}
: null } + { showModuleResistances && m.getExplosiveResistance() ?
{translate('explres')}: {formats.pct(m.getExplosiveResistance())}
: null } + { showModuleResistances && m.getKineticResistance() ?
{translate('kinres')}: {formats.pct(m.getKineticResistance())}
: null } + { showModuleResistances && m.getThermalResistance() ?
{translate('thermres')}: {formats.pct(m.getThermalResistance())}
: null } { validMods.length > 0 ?
: null }
diff --git a/src/app/components/StandardSlotSection.jsx b/src/app/components/StandardSlotSection.jsx index 48e53656..e319f524 100644 --- a/src/app/components/StandardSlotSection.jsx +++ b/src/app/components/StandardSlotSection.jsx @@ -2,8 +2,8 @@ import React from 'react'; import cn from 'classnames'; import SlotSection from './SlotSection'; import StandardSlot from './StandardSlot'; +import Module from '../shipyard/Module'; import { diffDetails } from '../utils/SlotFunctions'; -import * as ModuleUtils from '../shipyard/ModuleUtils'; import * as ShipRoles from '../shipyard/ShipRoles'; import { stopCtxPropagation } from '../utils/UtilityFunctions'; @@ -114,7 +114,7 @@ export default class StandardSlotSection extends SlotSection { selected={currentMenu == st[0]} onChange={this.props.onChange} ship={ship} - warning={m => m.pgen < ship.powerRetracted} + warning={m => m instanceof Module ? m.getPowerGeneration() < ship.powerRetracted : m.pgen < ship.powerRetracted} />; slots[2] = m.maxmass < (ship.ladenMass - st[1].mass + m.mass)} + warning={m => m instanceof Module ? m.getMaxMass() < (ship.ladenMass - st[1].mass + m.mass) : m.maxmass < (ship.ladenMass - st[1].mass + m.mass)} />; @@ -161,7 +161,7 @@ export default class StandardSlotSection extends SlotSection { selected={currentMenu == st[4]} onChange={this.props.onChange} ship={ship} - warning= {m => m.engcap < ship.boostEnergy} + warning={m => m instanceof Module ? m.getEnginesCapacity() < ship.boostEnergy : m.engcap < ship.boostEnergy} />; slots[6] = + {this.svg()} + + ); + } + /** * Generate the SVG * @return {React.Component} SVG Contents @@ -507,6 +519,40 @@ export class ListModifications extends SvgIcon { } } +/** + * Modified (engineers) + */ +export class Modified extends SvgIcon { + /** + * Overriden view box + * @return {String} view box + */ + viewBox() { return '0 0 200 200'; } + + /** + * Render the Icon + * @return {React.Component} SVG Icon + */ + render() { + return ( + + {this.svg()} + + ); + } + + /** + * Generate the SVG + * @return {React.Component} SVG Contents + */ + svg() { + return + + + ; + } +} + /** * Hammer */ diff --git a/src/app/i18n/Language.jsx b/src/app/i18n/Language.jsx index da82304f..5a6a5ee4 100644 --- a/src/app/i18n/Language.jsx +++ b/src/app/i18n/Language.jsx @@ -5,6 +5,7 @@ import * as ES from './es'; import * as FR from './fr'; import * as IT from './it'; import * as RU from './ru'; +import * as PL from './pl'; import d3 from 'd3'; let fallbackTerms = EN.terms; @@ -23,6 +24,7 @@ export function getLanguage(langCode) { case 'fr': lang = FR; break; case 'it': lang = IT; break; case 'ru': lang = RU; break; + case 'pl': lang = PL; break; default: lang = EN; } @@ -82,5 +84,6 @@ export const Languages = { it: 'Italiano', es: 'Español', fr: 'Français', - ru: 'ру́сский' + ru: 'ру́сский', + pl: 'polski' }; diff --git a/src/app/i18n/en.js b/src/app/i18n/en.js index 6276f0d1..b8b7f0ff 100644 --- a/src/app/i18n/en.js +++ b/src/app/i18n/en.js @@ -29,6 +29,8 @@ export const terms = { PHRASE_UNLADEN: 'Ship mass excluding fuel and cargo', PHRASE_UPDATE_RDY: 'Update Available! Click to refresh', + HELP_MODIFICATIONS_MENU: 'Click on a number to enter a new value, or drag along the bar for small changes', + // Other languages fallback to these values // Only Translate to other languages if the name is different in-game am: 'Auto Field-Maintenance Unit', @@ -92,14 +94,18 @@ export const terms = { // Unit for seconds secs: 's', - // Hardpoint abbreviations + // Weapon, offence and defence dpe: 'Damage per MJ of energy', dps: 'Damage per second', + sdps: 'Sustained damage per second', dpssdps: 'Damage per second (sustained damage per second)', eps: 'Energy per second', epsseps: 'Energy per second (sustained energy per second)', hps: 'Heat per second', hpsshps: 'Heat per second (sustained heat per second)', + 'damage by': 'Damage by', + 'damage from': 'Damage from', + 'shield cells': 'Shield cells', // Modifications ammo: 'Ammunition maximum', @@ -134,6 +140,7 @@ export const terms = { rof: 'Rate of fire', shield: 'Shield', shieldboost: 'Shield boost', + shieldreinforcement: 'Shield reinforcement', spinup: 'Spin up time', syscap: 'Systems capacity', sysrate: 'Systems recharge rate', diff --git a/src/app/i18n/pl.js b/src/app/i18n/pl.js new file mode 100644 index 00000000..8444532e --- /dev/null +++ b/src/app/i18n/pl.js @@ -0,0 +1,77 @@ +export const formats = { + decimal: '.', + thousands: ',', + grouping: [3], + currency: ['$', ''], + dateTime: '%a %b %e %X %Y', + date: '%m/%d/%Y', + time: '%H:%M:%S', + periods: ['AM', 'PM'], + days: ['Niedziela', 'Poniedziałek', 'Wtorek', 'Środa', 'Czwartek', 'Piątek', 'Sobota'], + shortDays: ['Nie', 'Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob'], + months: ['Styczeń', 'Luty', 'Marzec', 'Kwiecień', 'Maj', 'Czerwiec', 'Lipiec', 'Sierpień', 'Wrzesień', 'Październik', 'Listopad', 'Grudzień'], + shortMonths: ['Sty', 'Lut', 'Mar', 'Kwi', 'Maj', 'Cze', 'Lip', 'Sie', 'Wrz', 'Paź', 'Lis', 'Gru'] +}; + +export const terms = { + PHRASE_ALT_ALL: 'Alt + kliknięcie by wypełnić wszystkie sloty', + PHRASE_BACKUP_DESC: 'Kopia zapasowa wszystkich danych Coriolis w celu zapisu lub przeniesienia na inne urządzenie/przeglądarkę', + PHRASE_CONFIRMATION: 'Czy jesteś pewien?', + PHRASE_EXPORT_DESC: 'Szczegółowy eksport schematu w formacie JSON w celu użycia na innych stronach i narzędziach', + PHRASE_FASTEST_RANGE: 'Maksymalna ilość skoków na najwyższym zasięgu', + PHRASE_IMPORT: 'Wklej tu JSON lub importuj', + PHRASE_LADEN: 'Masa statku + paliwo + ładunek', + PHRASE_NO_BUILDS: 'Nie dodano schematu do porównania!', + PHRASE_NO_RETROCH: 'Brak zmian retrofit', + PHRASE_SELECT_BUILDS: 'Wybierz schematy do porównania', + PHRASE_SG_RECHARGE: 'Czas od 50% do 100% naładowania', + PHRASE_SG_RECOVER: 'Odnowienie (do 50%) po upadku', + PHRASE_UNLADEN: 'Masa statku z wyłączeniem paliwa i ładunku', + PHRASE_UPDATE_RDY: 'Dostępna aktualizacja! Naciśnij by odświeżyć', + + // Other languages fallback to these values + // Only Translate to other languages if the name is different in-game + am: 'Auto Field-Maintenance Unit', + bh: 'Bulkheads', + bl: 'Beam Laser', + bsg: 'Bi-Weave Shield Generator', + c: 'Cannon', + cc: 'Collector Limpet Controller', + cm: 'Countermeasure', + cr: 'Cargo Rack', + cs: 'Cargo Scanner', + dc: 'Docking Computer', + fc: 'Fragment Cannon', + fi: 'FSD Interdictor', + fs: 'Fuel Scoop', + fsd: 'Frame Shift Drive', + ft: 'Fuel Tank', + fx: 'Fuel Transfer Limpet Controller', + hb: 'Hatch Breaker Limpet Controller', + hr: 'Hull Reinforcement Package', + kw: 'Kill Warrant Scanner', + ls: 'Life Support', + mc: 'Multi-cannon', + ml: 'Mining Laser', + mr: 'Missile Rack', + nl: 'Mine Launcher', + pa: 'Plasma Accelerator', + pas: 'Planetary Approach Suite', + pc: 'Prospector Limpet Controller', + pd: 'power distributor', + pl: 'Pulse Laser', + pp: 'Power Plant', + psg: 'Prismatic Shield Generator', + pv: 'Planetary Vehicle Hangar', + rf: 'Refinery', + rg: 'Rail Gun', + s: 'Sensors', + sb: 'Shield Booster', + sc: 'Scanner', + scb: 'Shield Cell Bank', + sg: 'Shield Generator', + t: 'thrusters', + tp: 'Torpedo Pylon', + ul: 'Burst Laser', + ws: 'Frame Shift Wake Scanner' +}; diff --git a/src/app/pages/ComparisonPage.jsx b/src/app/pages/ComparisonPage.jsx index 5531525f..b258247e 100644 --- a/src/app/pages/ComparisonPage.jsx +++ b/src/app/pages/ComparisonPage.jsx @@ -345,7 +345,7 @@ export default class ComparisonPage extends Page { let code = fromComparison(name, builds, selectedFacets, predicate, desc); let loc = window.location; - return `${loc.protocol}//${loc.host}/comparison/${code}`; + return loc.protocol + '//' + loc.host + '/comparison?code=' + encodeURIComponent(code); } /** diff --git a/src/app/pages/ErrorDetails.jsx b/src/app/pages/ErrorDetails.jsx index 61be46ad..da100d3b 100644 --- a/src/app/pages/ErrorDetails.jsx +++ b/src/app/pages/ErrorDetails.jsx @@ -26,7 +26,7 @@ export default class ErrorDetails extends React.Component { if (ed) { content =
- Create an issue on Github + Create an issue on Github {' if this keeps happening. Add these details:'}
diff --git a/src/app/pages/OutfittingPage.jsx b/src/app/pages/OutfittingPage.jsx index 839c6b00..6dd3ba3a 100644 --- a/src/app/pages/OutfittingPage.jsx +++ b/src/app/pages/OutfittingPage.jsx @@ -8,13 +8,14 @@ import Persist from '../stores/Persist'; import Ship from '../shipyard/Ship'; import { toDetailedBuild } from '../shipyard/Serializer'; import { outfitURL } from '../utils/UrlGenerators'; - import { FloppyDisk, Bin, Switch, Download, Reload, Fuel } from '../components/SvgIcons'; import ShipSummaryTable from '../components/ShipSummaryTable'; import StandardSlotSection from '../components/StandardSlotSection'; import HardpointsSlotSection from '../components/HardpointsSlotSection'; import InternalSlotSection from '../components/InternalSlotSection'; import UtilitySlotSection from '../components/UtilitySlotSection'; +import OffenceSummary from '../components/OffenceSummary'; +import DefenceSummary from '../components/DefenceSummary'; import LineChart from '../components/LineChart'; import PowerManagement from '../components/PowerManagement'; import CostSection from '../components/CostSection'; @@ -288,12 +289,14 @@ export default class OutfittingPage extends Page { sStr = ship.getStandardString() + '.' + ship.getModificationsString(), iStr = ship.getInternalString() + '.' + ship.getModificationsString(); + Router.replace(outfitURL(ship.id, code, buildName)); + return (

{ship.name}

- + @@ -324,31 +327,11 @@ export default class OutfittingPage extends Page {
-

{translate('jump range')}

- +
-

{translate('total range')}

- +
@@ -397,3 +380,16 @@ export default class OutfittingPage extends Page { ); } } +//
+//

{translate('jump range')}

+// +//
diff --git a/src/app/pages/ShipyardPage.jsx b/src/app/pages/ShipyardPage.jsx index fe6bcae6..f794eb10 100644 --- a/src/app/pages/ShipyardPage.jsx +++ b/src/app/pages/ShipyardPage.jsx @@ -55,6 +55,7 @@ function shipSummary(shipId, shipData) { ship.optimizeMass({ th: ship.standard[1].maxClass + 'A', fsd: '2D', ft: '1C' }); // Optmize mass with Max Thrusters summary.topSpeed = ship.topSpeed; summary.topBoost = ship.topBoost; + summary.baseArmour = ship.armour; return summary; } @@ -141,7 +142,7 @@ export default class ShipyardPage extends Page {
- + diff --git a/src/app/shipyard/Constants.js b/src/app/shipyard/Constants.js index 566bcf06..e8714216 100755 --- a/src/app/shipyard/Constants.js +++ b/src/app/shipyard/Constants.js @@ -51,15 +51,19 @@ export const ModuleGroupToName = { bl: 'Beam Laser', ul: 'Burst Laser', c: 'Cannon', + ch: 'Chaff Launcher', cs: 'Cargo Scanner', cm: 'Countermeasure', + ec: 'Electronic Countermeasure', fc: 'Fragment Cannon', + hs: 'Heat Sink Launcher', ws: 'Frame Shift Wake Scanner', kw: 'Kill Warrant Scanner', nl: 'Mine Launcher', ml: 'Mining Laser', mr: 'Missile Rack', pa: 'Plasma Accelerator', + po: 'Point Defence', mc: 'Multi-cannon', pl: 'Pulse Laser', rg: 'Rail Gun', @@ -121,7 +125,7 @@ export const ShipFacets = [ }, { // 3 title: 'shields', - props: ['shieldStrength'], + props: ['shield'], unit: 'MJ', fmt: 'int', i: 3 diff --git a/src/app/shipyard/Module.js b/src/app/shipyard/Module.js index d367cc9a..d995ef16 100755 --- a/src/app/shipyard/Module.js +++ b/src/app/shipyard/Module.js @@ -11,7 +11,7 @@ export default class Module { * @param {Object} params Module parameters. Either grp/id or template */ constructor(params) { - let properties = Object.assign({ grp: null, id: null, template: null }, params); + let properties = Object.assign({ grp: null, id: null, template: null, }, params); let template; if (properties.template == undefined) { @@ -23,48 +23,65 @@ export default class Module { for (let p in template) { this[p] = template[p]; } } } - this.mods = {}; } /** * Get a value for a given modification * @param {Number} name The name of the modification - * @return {Number} The value of the modification, as a decimal value where 1 is 100% + * @return {Number} The value of the modification, as an integer value scaled so that 1.23% == 123 */ getModValue(name) { - return this.mods && this.mods[name] ? this.mods[name] / 10000 : null; + return this.mods && this.mods[name] ? this.mods[name] : null; } /** * Set a value for a given modification ID * @param {Number} name The name of the modification - * @param {Number} value The value of the modification, as a decimal value where 1 is 100% + * @param {Number} value The value of the modification, as an integer scaled so that -2.34% == -234 */ setModValue(name, value) { + if (!this.mods) { + this.mods = {}; + } + if (value == null || value == 0) { delete this.mods[name]; } else { - // Store value with 2dp - this.mods[name] = Math.round(value * 10000); + // Round just to be sure + this.mods[name] = Math.round(value); } } /** * Helper to obtain a modified value using standard multipliers - * @param {String} name the name of the modifier to obtain - * @return {Number} the mass of this module - */ - _getModifiedValue(name) { - let result = 0; - if (this[name]) { - result = this[name]; - if (result) { - let mult = this.getModValue(name); - if (mult) { result = result * (1 + mult); } + * @param {String} name the name of the modifier to obtain + * @param {Boolean} additive Optional true if the value is additive rather than multiplicative + * @return {Number} the mass of this module + */ + _getModifiedValue(name, additive) { + let result = this[name] || (additive ? 0 : null); // Additive NULL === 0 + if (result != null) { + // Jitter is special, being the only non-percentage value (it is in fact degrees) + const modValue = name === 'jitter' ? this.getModValue(name) / 100 : this.getModValue(name) / 10000; + if (modValue) { + if (additive) { + result = result + modValue; + } else { + result = result * (1 + modValue); + } } } return result; } + + /** + * Return true if this is a shield generator + * @return {Boolean} if this is a shield generator + */ + isShieldGenerator() { + return (this.grp === 'sg' || this.grp === 'psg' || this.grp === 'bsg'); + } + /** * Get the power generation of this module, taking in to account modifications * @return {Number} the power generation of this module @@ -190,7 +207,7 @@ export default class Module { * @return {Number} the kinetic resistance of this module */ getKineticResistance() { - return this._getModifiedValue('kinres'); + return this._getModifiedValue('kinres', true); } /** @@ -198,7 +215,7 @@ export default class Module { * @return {Number} the thermal resistance of this module */ getThermalResistance() { - return this._getModifiedValue('thermres'); + return this._getModifiedValue('thermres', true); } /** @@ -206,7 +223,7 @@ export default class Module { * @return {Number} the explosive resistance of this module */ getExplosiveResistance() { - return this._getModifiedValue('explres'); + return this._getModifiedValue('explres', true); } /** @@ -291,7 +308,7 @@ export default class Module { if (this['minmass']) { result = this['minmass']; if (result) { - let mult = this.getModValue('optmass'); + let mult = this.getModValue('optmass') / 10000; if (mult) { result = result * (1 + mult); } } } @@ -316,7 +333,7 @@ export default class Module { if (this['maxmass']) { result = this['maxmass']; if (result) { - let mult = this.getModValue('optmass'); + let mult = this.getModValue('optmass') / 10000; if (mult) { result = result * (1 + mult); } } } @@ -333,7 +350,7 @@ export default class Module { if (this['minmul']) { result = this['minmul']; if (result) { - let mult = this.getModValue('optmul'); + let mult = this.getModValue('optmul') / 10000; if (mult) { result = result * (1 + mult); } } } @@ -358,7 +375,7 @@ export default class Module { if (this['maxmul']) { result = this['maxmul']; if (result) { - let mult = this.getModValue('optmul'); + let mult = this.getModValue('optmul') / 10000; if (mult) { result = result * (1 + mult); } } } @@ -484,4 +501,44 @@ export default class Module { return this._getModifiedValue('hullboost'); } + /** + * Get the shield reinforcement for this module, taking in to account modifications + * @return {Number} the shield reinforcement for this module + */ + getShieldReinforcement() { + return this._getModifiedValue('shieldreinforcement'); + } + + /** + * Get the bays for this module, taking in to account modifications + * @return {Number} the bays for this module + */ + getBays() { + return this._getModifiedValue('bays'); + } + + /** + * Get the rebuilds per bay for this module, taking in to account modifications + * @return {Number} the rebuilds per bay for this module + */ + getRebuildsPerBay() { + return this._getModifiedValue('rebuildsperbay'); + } + + /** + * Get the cells for this module, taking in to account modifications + * @return {Number} the cells for this module + */ + getCells() { + return this._getModifiedValue('cells'); + } + + + /** + * Get the jitter for this module, taking in to account modifications + * @return {Number} the jitter for this module + */ + getJitter() { + return this._getModifiedValue('jitter', true); + } } diff --git a/src/app/shipyard/ModuleUtils.js b/src/app/shipyard/ModuleUtils.js index dc0b95e6..4949fd85 100755 --- a/src/app/shipyard/ModuleUtils.js +++ b/src/app/shipyard/ModuleUtils.js @@ -212,6 +212,16 @@ export function findHardpoint(groupName, clss, rating, name, mount, missile) { */ export function findHardpointId(groupName, clss, rating, name, mount, missile) { let h = this.findHardpoint(groupName, clss, rating, name, mount, missile); + if (h) { + return h.id; + } + + // Countermeasures used to be lumped in a single group but have been broken, out. If we have been given a groupName of 'Countermeasure' then + // rely on the unique name to find it + if (groupName === 'cm' || groupName === 'Countermeasure') { + h = this.findHardpoint(null, clss, rating, name, mount, missile); + } + return h ? h.id : 0; } diff --git a/src/app/shipyard/Serializer.js b/src/app/shipyard/Serializer.js index 1fb3e477..df2a1a7a 100644 --- a/src/app/shipyard/Serializer.js +++ b/src/app/shipyard/Serializer.js @@ -2,7 +2,9 @@ import { ModuleGroupToName, MountMap, BulkheadNames } from './Constants'; import { Ships } from 'coriolis-data/dist'; import Ship from './Ship'; import * as ModuleUtils from './ModuleUtils'; +import * as Utils from '../utils/UtilityFunctions'; import LZString from 'lz-string'; +import { outfitURL } from '../utils/UrlGenerators'; const STANDARD = ['powerPlant', 'thrusters', 'frameShiftDrive', 'lifeSupport', 'powerDistributor', 'sensors', 'fuelTank']; @@ -83,7 +85,7 @@ export function toDetailedBuild(buildName, ship) { ship: ship.name, references: [{ name: 'Coriolis.io', - url: `https://coriolis.edcd.io/outfit/${ship.id}/${code}?bn=${encodeURIComponent(buildName)}`, + url: 'https://coriolis.edcd.io' + outfitURL(ship.id, code, buildName), code, shipId: ship.id }], @@ -115,32 +117,12 @@ export function toDetailedBuild(buildName, ship) { return data; }; -/** - * Instantiates a ship from a ship-loadout object, using the code - * @param {Object} detailedBuild ship-loadout object - * @return {Ship} Ship instance - */ -export function fromDetailedBuild(detailedBuild) { - let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase()); - - if (!shipId) { - throw 'No such ship: ' + detailedBuild.ship; - } - - let comps = detailedBuild.components; - let stn = comps.standard; - let shipData = Ships[shipId]; - let ship = new Ship(shipId, shipData.properties, shipData.slots); - - return ship.buildFrom(detailedBuild.references[0].code); -}; - /** * Instantiates a ship from a ship-loadout object * @param {Object} detailedBuild ship-loadout object * @return {Ship} Ship instance */ -export function oldfromDetailedBuild(detailedBuild) { +export function fromDetailedBuild(detailedBuild) { let shipId = Object.keys(Ships).find((shipId) => Ships[shipId].properties.name.toLowerCase() == detailedBuild.ship.toLowerCase()); if (!shipId) { @@ -154,6 +136,7 @@ export function oldfromDetailedBuild(detailedBuild) { let shipData = Ships[shipId]; let ship = new Ship(shipId, shipData.properties, shipData.slots); let bulkheads = ModuleUtils.bulkheadIndex(stn.bulkheads); + let modifications = new Array(stn.bulkheads.modifications); if (bulkheads < 0) { throw 'Invalid bulkheads: ' + stn.bulkheads; @@ -165,6 +148,7 @@ export function oldfromDetailedBuild(detailedBuild) { } priorities.push(stn[c].priority === undefined ? 0 : stn[c].priority - 1); enabled.push(stn[c].enabled === undefined ? true : stn[c].enabled); + modifications.push(stn[c].modifications); return stn[c].class + stn[c].rating; }); @@ -185,8 +169,13 @@ export function oldfromDetailedBuild(detailedBuild) { comps.utility.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1), comps.internal.map(c => (!c || c.enabled === undefined) ? true : c.enabled * 1) ); + modifications = modifications.concat( + comps.hardpoints.map(c => (c && c.m ? c.m.modifications : null)), + comps.utility.map(c => (c && c.m ? c.m.modifications : null)), + comps.internal.map(c => (c && c.m ? c.m.modifications : null)) + ); - ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled); + ship.buildWith({ bulkheads, standard, hardpoints, internal }, priorities, enabled, modifications); return ship; }; @@ -228,7 +217,7 @@ export function fromComparison(name, builds, facets, predicate, desc) { f: facets, p: predicate, d: desc ? 1 : 0 - })).replace(/\//g, '-'); + })); }; /** @@ -237,5 +226,5 @@ export function fromComparison(name, builds, facets, predicate, desc) { * @return {Object} Comparison data object */ export function toComparison(code) { - return JSON.parse(LZString.decompressFromBase64(code.replace(/-/g, '/'))); + return JSON.parse(LZString.decompressFromBase64(Utils.fromUrlSafe(code))); }; diff --git a/src/app/shipyard/Ship.js b/src/app/shipyard/Ship.js index 25e35110..46f77500 100755 --- a/src/app/shipyard/Ship.js +++ b/src/app/shipyard/Ship.js @@ -1,5 +1,6 @@ import * as Calc from './Calculations'; import * as ModuleUtils from './ModuleUtils'; +import * as Utils from '../utils/UtilityFunctions'; import Module from './Module'; import LZString from 'lz-string'; import isEqual from 'lodash/lang'; @@ -126,7 +127,6 @@ export default class Ship { */ canBoost() { return this.canThrust() && // Thrusters operational - this.getSlotStatus(this.standard[4]) == 3 && // Power distributor operational this.boostEnergy <= this.standard[4].m.getEnginesCapacity(); // PD capacitor is sufficient for boost } @@ -194,10 +194,12 @@ export default class Ship { */ calcShieldRecovery() { if (this.shield > 0) { - let sgSlot = this.findInternalByGroup('sg'); - let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen'); - // 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts - return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15; + const sgSlot = this.findInternalByGroup('sg'); + if (sgSlot != null) { + let brokenRegenRate = 1 + sgSlot.m.getModValue('brokenregen') / 10000; + // 50% of shield strength / recovery recharge rate + 15 second delay before recharge starts + return ((this.shield / 2) / (sgSlot.m.recover * brokenRegenRate)) + 15; + } } return 0; } @@ -210,10 +212,12 @@ export default class Ship { */ calcShieldRecharge() { if (this.shield > 0) { - let sgSlot = this.findInternalByGroup('sg'); - let regenRate = 1 + sgSlot.m.getModValue('regen'); - // 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s - return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate); + const sgSlot = this.findInternalByGroup('sg'); + if (sgSlot != null) { + let regenRate = 1 + sgSlot.m.getModValue('regen') / 10000; + // 50% -> 100% recharge time, Bi-Weave shields charge at 1.8 MJ/s + return (this.shield / 2) / ((sgSlot.m.grp == 'bsg' ? 1.8 : 1) * regenRate); + } } return 0; } @@ -233,9 +237,8 @@ export default class Ship { sg = sgSlot.m; } - // TODO obtain shield boost - // return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, this.shieldMultiplier + (multiplierDelta || 0)); - return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 0 + (multiplierDelta || 0)); + // TODO Not accurate if the ship has modified shield boosters + return Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sg, 1 + (multiplierDelta || 0)); } /** @@ -397,19 +400,24 @@ export default class Ship { * Set a modification value * @param {Object} m The module to change * @param {Object} name The name of the modification to change - * @param {Number} value The new value of the modification + * @param {Number} value The new value of the modification. The value of the modification is scaled to provide two decimal places of precision in an integer. For example 1.23% is stored as 123 */ setModification(m, name, value) { + if (isNaN(value)) { + // Value passed is invalid; reset it to 0 + value = 0; + } + // Handle special cases - if (name == 'pgen') { + if (name === 'pgen') { // Power generation m.setModValue(name, value); this.updatePowerGenerated(); - } else if (name == 'power') { + } else if (name === 'power') { // Power usage m.setModValue(name, value); this.updatePowerUsed(); - } else if (name == 'mass') { + } else if (name === 'mass') { // Mass let oldMass = m.getMass(); m.setModValue(name, value); @@ -418,25 +426,40 @@ export default class Ship { this.ladenMass = this.ladenMass - oldMass + newMass; this.updateTopSpeed(); this.updateJumpStats(); - } else if (name == 'maxfuel') { + } else if (name === 'maxfuel') { m.setModValue(name, value); this.updateJumpStats(); - } else if (name == 'optmass') { + } else if (name === 'optmass') { m.setModValue(name, value); - // Could be for either thrusters or FSD + // Could be for any of thrusters, FSD or shield this.updateTopSpeed(); this.updateJumpStats(); - } else if (name == 'optmul') { + this.recalculateShield(); + } else if (name === 'optmul') { m.setModValue(name, value); - // Could be for either thrusters or FSD + // Could be for any of thrusters, FSD or shield this.updateTopSpeed(); this.updateJumpStats(); - } else if (name == 'shieldboost') { + this.recalculateShield(); + } else if (name === 'shieldboost') { + m.setModValue(name, value); + this.recalculateShield(); + } else if (name === 'hullboost' || name === 'hullreinforcement') { m.setModValue(name, value); - this.updateShield(); - } else if (name == 'hullboost') { + this.recalculateArmour(); + } else if (name === 'shieldreinforcement') { m.setModValue(name, value); - this.updateArmour(); + this.recalculateShieldCells(); + } else if (name === 'burst' || name === 'clip' || name === 'damage' || name === 'distdraw' || name === 'jitter' || name === 'piercing' || name === 'range' || name === 'reload' || name === 'rof' || name === 'thermload') { + m.setModValue(name, value); + this.recalculateDps(); + this.recalculateHps(); + this.recalculateEps(); + } else if (name === 'explres' || name === 'kinres' || name === 'thermres') { + m.setModValue(name, value); + // Could be for shields or armour + this.recalculateArmour(); + this.recalculateShield(); } else { // Generic m.setModValue(name, value); @@ -464,9 +487,21 @@ export default class Ship { this.ladenMass = 0; this.armour = this.baseArmour; this.shield = this.baseShieldStrength; + this.shieldCells = 0; this.totalCost = this.m.incCost ? this.m.discountedCost : 0; this.unladenMass = this.hullMass; + this.totalDpe = 0; + this.totalExplDpe = 0; + this.totalKinDpe = 0; + this.totalThermDpe = 0; this.totalDps = 0; + this.totalExplDps = 0; + this.totalKinDps = 0; + this.totalThermDps = 0; + this.totalSDps = 0; + this.totalExplSDps = 0; + this.totalKinSDps = 0; + this.totalThermSDps = 0; this.totalEps = 0; this.totalHps = 0; this.shieldExplRes = 0; @@ -481,7 +516,6 @@ export default class Ship { this.bulkheads.m.mods = mods && mods[0] ? mods[0] : {}; this.cargoHatch.priority = priorities ? priorities[0] * 1 : 0; this.cargoHatch.enabled = enabled ? enabled[0] * 1 : true; - this.cargoHatch.mods = mods ? mods[0] : {}; for (i = 0; i < cl; i++) { standard[i].cat = 0; @@ -538,8 +572,12 @@ export default class Ship { this.updatePowerGenerated() .updatePowerUsed() .updateJumpStats() - .updateShield() - .updateArmour() + .recalculateShield() + .recalculateShieldCells() + .recalculateArmour() + .recalculateDps() + .recalculateEps() + .recalculateHps() .updateTopSpeed(); } @@ -557,27 +595,27 @@ export default class Ship { let standard = new Array(this.standard.length), hardpoints = new Array(this.hardpoints.length), internal = new Array(this.internal.length), - mods = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length), + modifications = new Array(1 + this.standard.length + this.hardpoints.length + this.internal.length), parts = serializedString.split('.'), priorities = null, enabled = null, code = parts[0]; if (parts[1]) { - enabled = LZString.decompressFromBase64(parts[1].replace(/-/g, '/')).split(''); + enabled = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[1])).split(''); } if (parts[2]) { - priorities = LZString.decompressFromBase64(parts[2].replace(/-/g, '/')).split(''); + priorities = LZString.decompressFromBase64(Utils.fromUrlSafe(parts[2])).split(''); } if (parts[3]) { - const modstr = parts[3].replace(/-/g, '/'); + const modstr = parts[3]; if (modstr.match(':')) { - this.decodeModificationsString(modstr, mods); + this.decodeModificationsString(modstr, modifications); } else { try { - this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(modstr, 'base64')), mods); + this.decodeModificationsStruct(zlib.gunzipSync(new Buffer(Utils.fromUrlSafe(modstr), 'base64')), modifications); } catch (err) { // Could be out-of-date URL; ignore } @@ -595,7 +633,7 @@ export default class Ship { }, priorities, enabled, - mods + modifications ); }; @@ -681,21 +719,27 @@ export default class Ship { if (slot.enabled != enabled) { // Enabled state is changing slot.enabled = enabled; if (slot.m) { - if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp == 'sb') { - this.updateShield(); + if (ModuleUtils.isShieldGenerator(slot.m.grp) || slot.m.grp === 'sb') { + this.recalculateShield(); } - if (slot.m.getDps()) { - this.totalDps += slot.m.getDps() * (enabled ? 1 : -1); + if (slot.m.grp === 'scb') { + this.recalculateShieldCells(); } - if (slot.m.getEps()) { - this.totalEps += slot.m.getEps() * (enabled ? 1 : -1); + + this.updatePowerUsed(); + this.updatePowerEnabledString(); + + if (slot.m.getDps()) { + this.recalculateDps(); } + if (slot.m.getHps()) { - this.totalHps += slot.m.getHps() * (enabled ? 1 : -1); + this.recalculateHps(); } - this.updatePowerUsed(); - this.updatePowerEnabledString(); + if (slot.m.getEps()) { + this.recalculateEps(); + } } } return this; @@ -732,10 +776,15 @@ export default class Ship { updateStats(slot, n, old, preventUpdate) { let powerGeneratedChange = slot == this.standard[0]; let powerUsedChange = false; + let dpsChanged = n && n.getDps() || old && old.getDps(); + let epsChanged = n && n.getEps() || old && old.getEps(); + let hpsChanged = n && n.getHps() || old && old.getHps(); - let armourChange = (slot == this.bulkheads) || (n && n.grp == 'hr') || (old && old.grp == 'hr'); + let armourChange = (slot === this.bulkheads) || (n && n.grp === 'hr') || (old && old.grp === 'hr'); - let shieldChange = (n && n.grp == 'bsg') || (old && old.grp == 'bsg') || (n && n.grp == 'psg') || (old && old.grp == 'psg') || (n && n.grp == 'sg') || (old && old.grp == 'sg') || (n && n.grp == 'sb') || (old && old.grp == 'sb'); + let shieldChange = (n && n.grp === 'bsg') || (old && old.grp === 'bsg') || (n && n.grp === 'psg') || (old && old.grp === 'psg') || (n && n.grp === 'sg') || (old && old.grp === 'sg') || (n && n.grp === 'sb') || (old && old.grp === 'sb'); + + let shieldCellsChange = (n && n.grp === 'scb') || (old && old.grp === 'scb'); if (old) { // Old modul now being removed switch (old.grp) { @@ -755,16 +804,6 @@ export default class Ship { powerUsedChange = true; } - if (old.getDps()) { - this.totalDps -= old.getDps(); - } - if (old.getEps()) { - this.totalEps -= old.getEps(); - } - if (old.getHps()) { - this.totalHps -= old.getHps(); - } - this.unladenMass -= old.getMass() || 0; } @@ -786,22 +825,21 @@ export default class Ship { powerUsedChange = true; } - if (n.getDps()) { - this.totalDps += n.getDps(); - } - if (n.getEps()) { - this.totalEps += n.getEps(); - } - if (n.getHps()) { - this.totalHps += n.getHps(); - } - this.unladenMass += n.getMass() || 0; } this.ladenMass = this.unladenMass + this.cargoCapacity + this.fuelCapacity; if (!preventUpdate) { + if (dpsChanged) { + this.recalculateDps(); + } + if (epsChanged) { + this.recalculateEps(); + } + if (hpsChanged) { + this.recalculateHps(); + } if (powerGeneratedChange) { this.updatePowerGenerated(); } @@ -809,10 +847,13 @@ export default class Ship { this.updatePowerUsed(); } if (armourChange) { - this.updateArmour(); + this.recalculateArmour(); } if (shieldChange) { - this.updateShield(); + this.recalculateShield(); + } + if (shieldCellsChange) { + this.recalculateShieldCells(); } this.updateTopSpeed(); this.updateJumpStats(); @@ -820,6 +861,148 @@ export default class Ship { return this; } + /** + * Calculate diminishing returns value, where values below a given limit are returned + * as-is, and values between the lower and upper limit of the diminishing returns are + * given at half value. + * Commonly used for resistances. + * @param {Number} val The value + * @param {Number} drll The lower limit for diminishing returns + * @param {Number} drul The upper limit for diminishing returns + * @return {this} The ship instance (for chaining operations) + */ + diminishingReturns(val, drll, drul) { + if (val > drll) { + val = drll + (val - drll) / 2; + } + if (val > drul) { + val = drul; + } + return val; + } + + /** + * Calculate damage per second for weapons + * @return {this} The ship instance (for chaining operations) + */ + recalculateDps() { + let totalDpe = 0; + let totalExplDpe = 0; + let totalKinDpe = 0; + let totalThermDpe = 0; + let totalDps = 0; + let totalExplDps = 0; + let totalKinDps = 0; + let totalThermDps = 0; + let totalSDps = 0; + let totalExplSDps = 0; + let totalKinSDps = 0; + let totalThermSDps = 0; + + for (let slotNum in this.hardpoints) { + const slot = this.hardpoints[slotNum]; + if (slot.m && slot.enabled && slot.m.getDps()) { + const dpe = slot.m.getDps() / slot.m.getEps(); + const dps = slot.m.getDps(); + const sdps = slot.m.getClip() ? (slot.m.getClip() * slot.m.getDps() / slot.m.getRoF()) / ((slot.m.getClip() / slot.m.getRoF()) + slot.m.getReload()) : dps; + + totalDpe += dpe; + totalDps += dps; + totalSDps += sdps; + if (slot.m.type === 'E') { + totalExplDpe += dpe; + totalExplDps += dps; + totalExplSDps += sdps; + } + if (slot.m.type === 'K') { + totalKinDpe += dpe; + totalKinDps += dps; + totalKinSDps += sdps; + } + if (slot.m.type === 'T') { + totalThermDpe += dpe; + totalThermDps += dps; + totalThermSDps += sdps; + } + if (slot.m.type === 'EK') { + totalExplDpe += dpe / 2; + totalKinDpe += dpe / 2; + totalExplDps += dps / 2; + totalKinDps += dps / 2; + totalExplSDps += sdps / 2; + totalKinSDps += sdps / 2; + } + if (slot.m.type === 'ET') { + totalExplDpe += dpe / 2; + totalThermDpe += dpe / 2; + totalExplDps += dps / 2; + totalThermDps += dps / 2; + totalExplSDps += sdps / 2; + totalThermSDps += sdps / 2; + } + if (slot.m.type === 'KT') { + totalKinDpe += dpe / 2; + totalThermDpe += dpe / 2; + totalKinDps += dps / 2; + totalThermDps += dps / 2; + totalKinSDps += sdps / 2; + totalThermSDps += sdps / 2; + } + } + } + + this.totalDpe = totalDpe; + this.totalExplDpe = totalExplDpe; + this.totalKinDpe = totalKinDpe; + this.totalThermDpe = totalThermDpe; + this.totalDps = totalDps; + this.totalExplDps = totalExplDps; + this.totalKinDps = totalKinDps; + this.totalThermDps = totalThermDps; + this.totalSDps = totalSDps; + this.totalExplSDps = totalExplSDps; + this.totalKinSDps = totalKinSDps; + this.totalThermSDps = totalThermSDps; + + return this; + } + + /** + * Calculate heat per second for weapons + * @return {this} The ship instance (for chaining operations) + */ + recalculateHps() { + let totalHps = 0; + + for (let slotNum in this.hardpoints) { + const slot = this.hardpoints[slotNum]; + if (slot.m && slot.enabled && slot.m.getHps()) { + totalHps += slot.m.getHps(); + } + } + this.totalHps = totalHps; + + return this; + } + + /** + * Calculate energy per second for weapons + * @return {this} The ship instance (for chaining operations) + */ + recalculateEps() { + let totalEps = 0; + + for (let slotNum in this.hardpoints) { + const slot = this.hardpoints[slotNum]; + if (slot.m && slot.enabled && slot.m.getEps()) { + totalEps += slot.m.getEps(); + } + } + this.totalEps = totalEps; + + return this; + } + /** * Update power calculations when amount generated changes * @return {this} The ship instance (for chaining operations) @@ -898,44 +1081,88 @@ export default class Ship { * Update shield * @return {this} The ship instance (for chaining operations) */ - updateShield() { - // Base shield from generator - let baseShield = 0; - let sgSlot = this.findInternalByGroup('sg'); + recalculateShield() { + let shield = 0; + let shieldExplRes = null; + let shieldKinRes = null; + let shieldThermRes = null; + + const sgSlot = this.findInternalByGroup('sg'); if (sgSlot && sgSlot.enabled) { - baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); + // Shield from generator + const baseShield = Calc.shieldStrength(this.hullMass, this.baseShieldStrength, sgSlot.m, 1); + shield = baseShield; + shieldExplRes = 1 - sgSlot.m.getExplosiveResistance(); + shieldKinRes = 1 - sgSlot.m.getKineticResistance(); + shieldThermRes = 1 - sgSlot.m.getThermalResistance(); + + // Shield from boosters + for (let slot of this.hardpoints) { + if (slot.m && slot.m.grp == 'sb') { + shield += baseShield * slot.m.getShieldBoost(); + shieldExplRes *= (1 - slot.m.getExplosiveResistance()); + shieldKinRes *= (1 - slot.m.getKineticResistance()); + shieldThermRes *= (1 - slot.m.getThermalResistance()); + } + } } - let shield = baseShield; + this.shield = shield; + this.shieldExplRes = shieldExplRes ? 1 - this.diminishingReturns(1 - shieldExplRes, 0.5, 0.75) : null; + this.shieldKinRes = shieldKinRes ? 1 - this.diminishingReturns(1 - shieldKinRes, 0.5, 0.75) : null; + this.shieldThermRes = shieldThermRes ? 1 - this.diminishingReturns(1 - shieldThermRes, 0.5, 0.75) : null; + + return this; + } - // Shield from boosters - for (let slot of this.hardpoints) { - if (slot.m && slot.m.grp == 'sb') { - shield += baseShield * slot.m.getShieldBoost(); + /** + * Update shield cells + * @return {this} The ship instance (for chaining operations) + */ + recalculateShieldCells() { + let shieldCells = 0; + + for (let slot of this.internal) { + if (slot.m && slot.m.grp == 'scb') { + shieldCells += slot.m.getShieldReinforcement() * slot.m.getCells(); } } - this.shield = shield; + + this.shieldCells = shieldCells; + return this; } /** - * Update armour + * Update armour and hull resistances * @return {this} The ship instance (for chaining operations) */ - updateArmour() { + recalculateArmour() { // Armour from bulkheads let bulkhead = this.bulkheads.m; let armour = this.baseArmour + (this.baseArmour * bulkhead.getHullBoost()); + let hullExplRes = 1 - bulkhead.getExplosiveResistance(); + let hullKinRes = 1 - bulkhead.getKineticResistance(); + let hullThermRes = 1 - bulkhead.getThermalResistance(); // Armour from HRPs for (let slot of this.internal) { if (slot.m && slot.m.grp == 'hr') { armour += slot.m.getHullReinforcement(); // Hull boost for HRPs is applied against the ship's base armour - armour += this.baseArmour * slot.m.getModValue('hullboost'); + armour += this.baseArmour * slot.m.getModValue('hullboost') / 10000; + + hullExplRes *= (1 - slot.m.getExplosiveResistance()); + hullKinRes *= (1 - slot.m.getKineticResistance()); + hullThermRes *= (1 - slot.m.getThermalResistance()); } } + this.armour = armour; + this.hullExplRes = 1 - this.diminishingReturns(1 - hullExplRes, 0.5, 0.75); + this.hullKinRes = 1 - this.diminishingReturns(1 - hullKinRes, 0.5, 0.75); + this.hullThermRes = 1 - this.diminishingReturns(1 - hullThermRes, 0.5, 0.75); + return this; } @@ -972,7 +1199,7 @@ export default class Ship { priorities.push(slot.priority); } - this.serialized.priorities = LZString.compressToBase64(priorities.join('')).replace(/\//g, '-'); + this.serialized.priorities = LZString.compressToBase64(priorities.join('')); return this; } @@ -993,21 +1220,21 @@ export default class Ship { enabled.push(slot.enabled ? 1 : 0); } - this.serialized.enabled = LZString.compressToBase64(enabled.join('')).replace(/\//g, '-'); + this.serialized.enabled = LZString.compressToBase64(enabled.join('')); return this; } /** - * Update the modifications string + * Update the modifications string in a human-readable format * @return {this} The ship instance (for chaining operations) */ - oldupdateModificationsString() { + debugupdateModificationsString() { let allMods = new Array(); 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) + ':' + Math.round(this.bulkheads.m.getModValue(modKey) * 10000)); + bulkheadMods.push(Modifications.modifications.indexOf(modKey) + ':' + this.bulkheads.m.getModValue(modKey)); } } allMods.push(bulkheadMods.join(';')); @@ -1016,7 +1243,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) + ':' + Math.round(slot.m.getModValue(modKey) * 10000)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); @@ -1025,7 +1252,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) + ':' + Math.round(slot.m.getModValue(modKey) * 10000)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); @@ -1034,12 +1261,12 @@ 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) + ':' + Math.round(slot.m.getModValue(modKey) * 10000)); + slotMods.push(Modifications.modifications.indexOf(modKey) + ':' + slot.m.getModValue(modKey)); } } allMods.push(slotMods.join(';')); } - this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, '')).replace(/\//g, '-'); + this.serialized.modifications = LZString.compressToBase64(allMods.join(',').replace(/,+$/, '')); return this; } @@ -1057,7 +1284,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]); } @@ -1071,7 +1298,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) */ @@ -1082,7 +1309,10 @@ 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({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(this.bulkheads.m.getModValue(modKey) * 10000) }); + // Filter out invalid modifications + if (Modifications.validity['bh'] && Modifications.validity['bh'].indexOf(modKey) != -1) { + bulkheadMods.push({ id: Modifications.modifications.indexOf(modKey), value: this.bulkheads.m.getModValue(modKey) }); + } } } slots.push(bulkheadMods); @@ -1091,7 +1321,10 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) }); + // Filter out invalid modifications + if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); + } } } slots.push(slotMods); @@ -1101,7 +1334,10 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) }); + // Filter out invalid modifications + if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); + } } } slots.push(slotMods); @@ -1111,7 +1347,10 @@ export default class Ship { let slotMods = new Array(); if (slot.m && slot.m.mods) { for (let modKey in slot.m.mods) { - slotMods.push({ id: Modifications.modifiers.indexOf(modKey), value: Math.round(slot.m.getModValue(modKey) * 10000) }); + // Filter out invalid modifications + if (Modifications.validity[slot.m.grp] && Modifications.validity[slot.m.grp].indexOf(modKey) != -1) { + slotMods.push({ id: Modifications.modifications.indexOf(modKey), value: slot.m.getModValue(modKey) }); + } } } slots.push(slotMods); @@ -1137,6 +1376,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.modifications[slotMod.id] + ' = ' + slotMod.value); curpos += 4; } buffer.writeInt8(-1, curpos++); @@ -1147,7 +1387,7 @@ export default class Ship { buffer.writeInt8(-1, curpos++); } - this.serialized.modifications = zlib.gzipSync(buffer).toString('base64').replace(/\//g, '-'); + this.serialized.modifications = zlib.gzipSync(buffer).toString('base64'); } else { this.serialized.modifications = null; } @@ -1169,7 +1409,8 @@ export default class Ship { while (modificationId != -1) { let modificationValue = buffer.readInt32LE(curpos); curpos += 4; - 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; @@ -1219,7 +1460,6 @@ export default class Ship { case 1: this.serialized.hardpoints = null; break; case 2: this.serialized.internal = null; } - this.serialized.modifications = null; } return this; } diff --git a/src/app/stores/Persist.js b/src/app/stores/Persist.js index 63473691..4ef740f6 100644 --- a/src/app/stores/Persist.js +++ b/src/app/stores/Persist.js @@ -11,6 +11,7 @@ const LS_KEY_MOD_DISCOUNT = 'moduleDiscount'; const LS_KEY_STATE = 'state'; const LS_KEY_SIZE_RATIO = 'sizeRatio'; const LS_KEY_TOOLTIPS = 'tooltips'; +const LS_KEY_MODULE_RESISTANCES = 'moduleResistances'; let LS; @@ -81,6 +82,7 @@ export class Persist extends EventEmitter { LS = null; } + let moduleResistances = _get(LS_KEY_MODULE_RESISTANCES); let tips = _get(LS_KEY_TOOLTIPS); let insurance = _getString(LS_KEY_INSURANCE); let shipDiscount = _get(LS_KEY_SHIP_DISCOUNT); @@ -99,6 +101,7 @@ export class Persist extends EventEmitter { this.state = _get(LS_KEY_STATE); this.sizeRatio = _get(LS_KEY_SIZE_RATIO) || 1; this.tooltipsEnabled = tips === null ? true : tips; + this.moduleResistancesEnabled = moduleResistances === null ? true : moduleResistances; if (LS) { window.addEventListener('storage', this.onStorageChange); @@ -143,6 +146,10 @@ export class Persist extends EventEmitter { this.tooltipsEnabled = !!newValue && newValue.toLowerCase() == 'true'; this.emit('tooltips', this.tooltipsEnabled); break; + case LS_KEY_MODULE_RESISTANCES: + this.moduleResistancesEnabled = !!newValue && newValue.toLowerCase() == 'true'; + this.emit('moduleresistances', this.moduleResistancesEnabled); + break; } } catch (e) { // On JSON.Parse Error - don't sync or do anything @@ -183,6 +190,21 @@ export class Persist extends EventEmitter { return this.tooltipsEnabled; } + /** + * Show module resistances setting + * @param {boolean} show Optional - update setting + * @return {boolean} True if module resistances should be shown + */ + showModuleResistances(show) { + if (show !== undefined) { + this.moduleResistancesEnabled = !!show; + _put(LS_KEY_MODULE_RESISTANCES, this.moduleResistancesEnabled); + this.emit('moduleresistances', this.moduleResistancesEnabled); + } + + return this.moduleResistancesEnabled; + } + /** * Persist a ship build in local storage. * diff --git a/src/app/utils/CompanionApiUtils.js b/src/app/utils/CompanionApiUtils.js new file mode 100644 index 00000000..c6c3b71f --- /dev/null +++ b/src/app/utils/CompanionApiUtils.js @@ -0,0 +1,367 @@ +import React from 'react'; +import { Modifications, Modules, Ships } from 'coriolis-data/dist'; +import Module from '../shipyard/Module'; +import Ship from '../shipyard/Ship'; + +// 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; + if (armourJson.modifiers) _addModifications(ship.bulkheads.m, armourJson.modifiers); + + // 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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; + + // 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 = 0; + + // 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 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]; + } + 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) / 10000; + if (!mod) { + mod = 0; + } + module.setModValue(action, ((1 + mod) * (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') / 10000); + module.setModValue('shieldboost', alteredBoost * 10000 / module.shieldboost); + } + + // Shield booster resistance is actually a damage modifier, so needs to be inverted. + if (module.grp === 'sb') { + if (module.getModValue('explres')) { + module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); + } + if (module.getModValue('kinres')) { + module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); + } + if (module.getModValue('thermres')) { + module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); + } + } + + // Shield generator resistance is actually a damage modifier, so needs to be inverted. + // In addition, the modification is based off the inherent resistance of the module + if (module.isShieldGenerator()) { + if (module.getModValue('explres')) { + module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); + } + if (module.getModValue('kinres')) { + module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000); + } + if (module.getModValue('thermres')) { + module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000); + } + } + + // Hull reinforcement package resistance is actually a damage modifier, so needs to be inverted. + if (module.grp === 'hr') { + if (module.getModValue('explres')) { + module.setModValue('explres', ((module.getModValue('explres') / 10000) * -1) * 10000); + } + if (module.getModValue('kinres')) { + module.setModValue('kinres', ((module.getModValue('kinres') / 10000) * -1) * 10000); + } + if (module.getModValue('thermres')) { + module.setModValue('thermres', ((module.getModValue('thermres') / 10000) * -1) * 10000); + } + } + + // Bulkhead resistance is actually a damage modifier, so needs to be inverted. + // In addition, the modification is based off the inherent resistance of the module + if (module.grp == 'bh') { + if (module.getModValue('explres')) { + module.setModValue('explres', ((1 - (1 - module.explres) * (1 + module.getModValue('explres') / 10000)) - module.explres) * 10000); + } + if (module.getModValue('kinres')) { + module.setModValue('kinres', ((1 - (1 - module.kinres) * (1 + module.getModValue('kinres') / 10000)) - module.kinres) * 10000); + } + if (module.getModValue('thermres')) { + module.setModValue('thermres', ((1 - (1 - module.thermres) * (1 + module.getModValue('thermres') / 10000)) - module.thermres) * 10000); + } + } + + // Jitter is an absolute number, so we need to divide it by 100 + 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('rof') / 10000)) - 1) * 10000); + } +} diff --git a/src/app/utils/SlotFunctions.js b/src/app/utils/SlotFunctions.js index 74697c7f..a9481315 100644 --- a/src/app/utils/SlotFunctions.js +++ b/src/app/utils/SlotFunctions.js @@ -93,69 +93,6 @@ export function slotComparator(translate, propComparator, desc) { }; } -const PROP_BLACKLIST = { - eddbID: 1, - edID: 1, - id: 1, - index: 1, - 'class': 1, - rating: 1, - maxfuel: 1, - fuelmul: 1, - fuelpower: 1, - optmass: 1, - maxmass: 1, - minmass: 1, - passive: 1, - thermload: 1, - ammocost: 1, - activepower: 1, - cooldown: 1, - chargeup: 1, - optmul: 1, - minmul: 1, - maxmul: 1, - ssdam: 1, - mjdps: 1, - mjeps: 1, - mass: 1, - cost: 1, - recover: 1, - wepcap: 1, - weprate: 1, - engcap: 1, - engrate: 1, - syscap: 1, - sysrate: 1, - breachdps: 1, - breachmin: 1, - breachmax: 1, - integrity: 1 -}; - -const TERM_LOOKUP = { - pgen: 'power', - armouradd: 'armour', - rof: 'ROF', - dps: 'DPS' -}; - -const FORMAT_LOOKUP = { - time: 'time' -}; - -const UNIT_LOOKUP = { - fuel: 'T', - cargo: 'T', - rate: 'kgs', - range: 'km', - recharge: 'MJ', - rangeLS: 'Ls', - power: 'MJ', - pgen: 'MJ', - rof: 'ps' -}; - /** * Determine the appropriate class based on diff value * @param {Number} a Potential Module (cannot be null) @@ -214,23 +151,15 @@ export function diffDetails(language, m, mm) { let mmMass = mm ? mm.getMass() : 0; if (mMass != mmMass) propDiffs.push(
{translate('mass')}: {diff(formats.round, mMass, mmMass)}{units.T}
); + let mPowerGeneration = m.pgen || 0; + let mmPowerGeneration = mm ? mm.getPowerGeneration() : 0; + if (mPowerGeneration != mmPowerGeneration) propDiffs.push(
{translate('pgen')}: {diff(formats.round, mPowerGeneration, mmPowerGeneration)}{units.MJ}
); + let mPowerUsage = m.power || 0; let mmPowerUsage = mm ? mm.getPowerUsage() : 0; if (mPowerUsage != mmPowerUsage) propDiffs.push(
{translate('power')}: {diff(formats.round, mPowerUsage, mmPowerUsage)}{units.MJ}
); -// for (let p in m) { -// if (!PROP_BLACKLIST[p] && !isNaN(m[p])) { -// let mVal = m[p] === null ? Infinity : m[p]; -// let mmVal = mm[p] === null ? Infinity : mm[p]; -// let format = formats[FORMAT_LOOKUP[p]] || formats.round; -// propDiffs.push(
-// {`${translate(TERM_LOOKUP[p] || p)}: `} -// {diff(format, mVal, mmVal)}{units[UNIT_LOOKUP[p]]} -//
); -// } -// } - - let mDps = m.damage * (m.rpshot || 1) * m.rof || 0; + let mDps = m.damage * (m.rpshot || 1) * (m.rof || 1) || 0; let mmDps = mm ? mm.getDps() || 0 : 0; if (mDps != mmDps) propDiffs.push(
{translate('dps')}: {diff(formats.round, mDps, mmDps)}
); diff --git a/src/app/utils/UrlGenerators.js b/src/app/utils/UrlGenerators.js index e599fd4d..827cc04e 100644 --- a/src/app/utils/UrlGenerators.js +++ b/src/app/utils/UrlGenerators.js @@ -1,4 +1,3 @@ - /** * Generates a URL for the outiffing page * @param {String} shipId Ship Id @@ -7,15 +6,18 @@ * @return {String} URL */ export function outfitURL(shipId, code, buildName) { - let parts = ['/outfit/', shipId]; + let path = '/outfit/' + shipId; + + let sepChar = '?'; if (code) { - parts.push('/', code); + path = path + sepChar + 'code=' + encodeURIComponent(code); + sepChar = '&'; } if (buildName) { - parts.push('?bn=', encodeURIComponent(buildName)); + path = path + sepChar + 'bn=' + encodeURIComponent(buildName); } - return parts.join(''); -} + return path; +} diff --git a/src/app/utils/UtilityFunctions.js b/src/app/utils/UtilityFunctions.js index fd51b434..1137af26 100644 --- a/src/app/utils/UtilityFunctions.js +++ b/src/app/utils/UtilityFunctions.js @@ -58,3 +58,16 @@ export function shallowEqual(objA, objB) { return true; } + +/** + * Turn a URL-safe base-64 encoded string in to a normal version. + * Coriolis used to use a different encoding system, and some old + * data might be bookmarked or on local storage, so we keep this + * around and use it when decoding data from the old-style URLs to + * be safe. + * @param {string} data the string + * @return {string} the converted string + */ +export function fromUrlSafe(data) { + return data ? data.replace(/-/g, '/').replace(/_/g, '+') : null; +} diff --git a/src/less/icons.less b/src/less/icons.less index 63c8316f..9af0c0e6 100755 --- a/src/less/icons.less +++ b/src/less/icons.less @@ -1,4 +1,5 @@ +// Standard icons .icon { display: inline-block; vertical-align: middle; @@ -25,3 +26,39 @@ height: 2em; } } + +// Modifiction icons - hard-code stroke/fill +.modicon { + display: inline-block; + vertical-align: middle; + width: 1.1em; + height: 1em; + stoke: @fg; + fill: transparent; + + &.sm { + width: 0.8em; + height: 0.75em; + } + + &.tn { + width: 0.6em; + height: 0.5em; + } + + &.lg { + width: 1.6em; + height: 1.5em; + } + + &.xl { + width: 2.1em; + height: 2em; + } +} + +.summary { + stroke: @fg; + stroke-width: 10; + fill: @fg; +} diff --git a/src/schemas/ship-loadout/4.json b/src/schemas/ship-loadout/4.json new file mode 100644 index 00000000..d3158f9f --- /dev/null +++ b/src/schemas/ship-loadout/4.json @@ -0,0 +1,356 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://cdn.coriolis.io/schemas/ship-loadout/4.json#", + "title": "Ship Loadout", + "type": "object", + "description": "The details for a specific ship build/loadout", + "required": ["name", "ship", "components"], + "properties": { + "name": { + "description": "The name of the build/loadout", + "type": "string", + "minLength": 1 + }, + "ship": { + "description": "The full display name of the ship", + "type": "string", + "minimum": 3 + }, + "manufacturer": { + "description": "The ship manufacturer", + "type": "string" + }, + "references" : { + "description": "3rd Party references and/or links to this build/loadout", + "type": "array", + "items": { + "type": "object", + "required": ["name","url"], + "additionalProperties": true, + "properties": { + "name": { + "description": "The name of the 3rd party, .e.g 'Coriolis.io' or 'E:D Shipyard'", + "type": "string" + }, + "url": { + "description": "The link/url to the 3rd party referencing this build/loadout", + "type": "string" + } + } + } + }, + "components": { + "description": "The components used by this build", + "type": "object", + "additionalProperties": false, + "required": ["standard", "internal", "hardpoints", "utility"], + "properties": { + "standard": { + "description": "The set of standard components across all ships", + "type": "object", + "additionalProperties": false, + "required": ["bulkheads", "powerPlant", "thrusters", "frameShiftDrive", "lifeSupport", "powerDistributor", "sensors", "fuelTank", "cargoHatch"], + "properties": { + "bulkheads": { + "enum": ["Lightweight Alloy", "Reinforced Alloy", "Military Grade Composite", "Mirrored Surface Composite", "Reactive Surface Composite"] + }, + "cargoHatch": { + "required": ["enabled", "priority"], + "properties": { + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 } + } + }, + "powerPlant": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 2, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + }, + "thrusters": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 2, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "name": { + "description": "The name identifing the thrusters (if applicable), e.g. 'Enhanced Performance'", + "type": "string" + }, + "modifications": { "type": "object" } + } + }, + "frameShiftDrive": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 2, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + }, + "lifeSupport": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 1, "maximum": 6 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + }, + "powerDistributor": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 1, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + }, + "sensors": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 1, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + }, + "fuelTank": { + "required": ["class", "rating", "enabled", "priority"], + "properties": { + "class": { "type": "integer", "minimum": 1, "maximum": 6 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "modifications": { "type": "object" } + } + } + } + }, + "internal": { + "type": "array", + "items": { + "type": ["object", "null"], + "required": ["class", "rating", "enabled", "priority", "group"], + "properties" : { + "class": { "type": "integer", "minimum": 1, "maximum": 8 }, + "rating": { "$ref": "#/definitions/standardRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "group": { + "description": "The group of the component, e.g. 'Shield Generator', or 'Cargo Rack'", + "type": "string" + }, + "name": { + "description": "The name identifying the component (if applicable), e.g. 'Advance Discovery Scanner', or 'Detailed Surface Scanner'", + "type": "string" + }, + "modifications": { "type": "object" } + } + }, + "minItems": 3 + }, + "hardpoints": { + "type": "array", + "items": { + "type": ["object", "null"], + "required": ["class", "rating", "enabled", "priority", "group", "mount"], + "properties" : { + "class": { "type": "integer", "minimum": 1, "maximum": 4 }, + "rating": { "$ref": "#/definitions/allRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "mount": { "type": "string", "enum": ["Fixed", "Gimballed", "Turret"] }, + "group": { + "description": "The group of the component, e.g. 'Beam Laser', or 'Missile Rack'", + "type": "string" + }, + "name": { + "description": "The name identifing the component (if applicable), e.g. 'Retributor', or 'Mining Lance'", + "type": "string" + }, + "modifications": { "type": "object" } + } + }, + "minItems": 1 + }, + "utility": { + "type": "array", + "items": { + "type": ["object", "null"], + "required": ["class", "rating", "enabled", "priority", "group"], + "properties" : { + "class": { "type": "integer", "minimum": 0, "maximum": 0 }, + "rating": { "$ref": "#/definitions/allRatings" }, + "enabled": { "type": "boolean" }, + "priority": { "type": "integer", "minimum": 1, "maximum": 5 }, + "group": { + "description": "The group of the component, e.g. 'Shield Booster', or 'Kill Warrant Scanner'", + "type": "string" + }, + "name": { + "description": "The name identifing the component (if applicable), e.g. 'Point Defence', or 'Electronic Countermeasure'", + "type": "string" + }, + "modifications": { "type": "object" } + } + }, + "minItems": 1 + } + } + }, + "stats": { + "description": "Optional statistics from the build", + "type": "object", + "additionalProperties": true, + "properties": { + "agility": { + "type": "integer", + "minimum": 0 + }, + "armour": { + "description": "Sum of base armour + any hull reinforcements", + "type": "integer", + "minimum": 1 + }, + "armourAdded":{ + "description": "Armour added through Hull reinforcement", + "type": "integer", + "minimum": 0 + }, + "baseShieldStrength": { + "type": "integer", + "minimum": 1 + }, + "baseArmour": { + "type": "integer", + "minimum": 1 + }, + "boost": { + "description": "Maximum boost speed of the ships (4 pips, straight-line)", + "type": "number", + "minimum": 0 + }, + "cargoCapacity": { + "type": "integer", + "minimum": 0 + }, + "class": { + "description": "Ship Class/Size [Small, Medium, Large]", + "enum": [1,2,3] + }, + "totalDps": { + "description": "Total damage dealt per second of all weapons", + "type": "number", + "minimum": 0 + }, + "totalEps": { + "description": "Total energy consumed per second of all weapons", + "type": "number", + "minimum": 0 + }, + "totalHps": { + "description": "Total heat generated per second of all weapons", + "type": "number", + "minimum": 0 + }, + "hullCost": { + "description": "Cost of the ship's hull", + "type": "integer", + "minimum": 1 + }, + "hullMass": { + "description": "Mass of the Ship hull only", + "type": "number", + "minimum": 1 + }, + "hullExplRes": { + "description": "Multiplier for explosive damage to hull", + "type": "number" + }, + "hullKinRes": { + "description": "Multiplier for kinetic damage to hull", + "type": "number" + }, + "hullThermRes": { + "description": "Multiplier for thermal damage to hull", + "type": "number" + }, + "fuelCapacity": { + "type": "integer", + "minimum": 1 + }, + "fullTankRange": { + "description": "Single Jump range with a full tank (unladenMass + fuel)", + "type": "number", + "minimum": 0 + }, + "ladenMass": { + "description": "Mass of the Ship + fuel + cargo (hull + all components + fuel tank + cargo capacity)", + "type": "number", + "minimum": 1 + }, + "ladenRange": { + "description": "Single Jump range with full cargo load, see ladenMass", + "type": "number", + "minimum": 0 + }, + "masslock": { + "description": "Mass Lock Factor of the Ship", + "type": "integer", + "minimum": 1 + }, + "shield": { + "description": "Shield strength in Mega Joules (Mj)", + "type": "number", + "minimum": 0 + }, + "shieldExplRes": { + "description": "Multiplier for explosive damage to shields", + "type": "number" + }, + "shieldKinRes": { + "description": "Multiplier for kinetic damage to shields", + "type": "number" + }, + "shieldThermRes": { + "description": "Multiplier for thermal damage to shields", + "type": "number" + }, + "speed": { + "description": "Maximum speed of the ships (4 pips, straight-line)", + "type": "number", + "minimum": 1 + }, + "totalCost": { + "description": "Total cost of the loadout, including discounts", + "type": "number" + }, + "unladenRange": { + "description": "Single Jump range when unladen, see unladenMass", + "type": "number", + "minimum": 0 + }, + "unladenMass": { + "description": "Mass of the Ship (hull + all components)", + "type": "number", + "minimum": 1 + } + } + } + }, + "definitions": { + "standardRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H"] }, + "allRatings": { "enum": ["A", "B", "C", "D", "E", "F", "G", "H", "I" ] } + } +}
{translate('EPS')} {translate('HPS')} {translate('armour')}{translate('shields')}{translate('shields')} {translate('mass')} {translate('cargo')} {translate('fuel')}{translate('MLF')}
{translate('strength')}{translate('recovery')}{translate('recharge')} {translate('hull')} {translate('unladen')} {translate('laden')}{f1(ship.totalHps)} {int(ship.armour)} {int(ship.shield)} {u.MJ}{sgRecover}{sgRecharge} {ship.hullMass} {u.T} {int(ship.unladenMass)} {u.T} {int(ship.ladenMass)} {u.T}{s.agility} {fInt(s.speed)}{u['m/s']} {fInt(s.boost)}{u['m/s']}{s.baseArmour}{fInt(s.baseArmour)} {fInt(s.baseShieldStrength)}{u.MJ} {fInt(s.topSpeed)}{u['m/s']} {fInt(s.topBoost)}{u['m/s']}