diff --git a/.storybook/fits.ts b/.storybook/fits.ts index 380eb84..d84513c 100644 --- a/.storybook/fits.ts +++ b/.storybook/fits.ts @@ -36,99 +36,30 @@ Hammerhead II x1 }; export const hashFits = { - Loki: "fit:v2:H4sIAAAAAAAACmWQMZJDMQhD+38WChBgwx32Ijtb5P5d4ONkdpLOTwhbFjKT6efx90uXk+pmJqE+I9YKEueblC0WyXpDvABrZy2OTTR8UTIPwtp4UIQApNz3C/5DkiRYbwA3RA70TowLINl+8qHMJoYBIxPgTLwnHAOb4FtKOlkuxJeSn0p95lORLwVVQ+i6n9FKI77nTbXqbntPpqgrKoWVEDXN2pNtY01tWBM8rcAjTj9OtaJ6aBV5+qHdM345owlT0hPutE6T0AEAAA==", + "Tornado v1": + "fit:v1:H4sIAAAAAAAACj3OsRVDIQwEwZwqXMAFnE7wUWw34gIM/Wd+ClA2m62LHZ9zfq/3d++z0UgMBthoF7rwiwBjGNisl1gyMJ6VUulBeDC1SnGlXmLJSir51YBxhYOSRdaSDJruLRyizMEWlpw9KYhz5d8f4N5qF/QAAAA=", + "Loki v2": + "fit:v2:H4sIAAAAAAAACmWQMZJDMQhD+38WChBgwx32Ijtb5P5d4ONkdpLOTwhbFjKT6efx90uXk+pmJqE+I9YKEueblC0WyXpDvABrZy2OTTR8UTIPwtp4UIQApNz3C/5DkiRYbwA3RA70TowLINl+8qHMJoYBIxPgTLwnHAOb4FtKOlkuxJeSn0p95lORLwVVQ+i6n9FKI77nTbXqbntPpqgrKoWVEDXN2pNtY01tWBM8rcAjTj9OtaJ6aBV5+qHdM345owlT0hPutE6T0AEAAA==", + "Loki v3": + "fit:v3:H4sIAAAAAAAACoWSwVLDMAxE7/0WHSzJsuUjNw5woV8ATSb1QAnTNjD8PTIQ0pJ4cpTf7s6srNM+vwGllBzc9c8ZNoe+GV5aGz4AgYgSws3unN/bK0TL6L5t8nAwIyZyXKFUqKYKZUvWWrAHjOJchQqgR08VGsAjUc0bi9fpf3qbu33Zg0SUkZH3qlcCWhPwmsCvCWRNEICdclgsYOVQ4nI5BdIofz52F8EPubPyjNHPvAVRQWG20YLYkOjMtR2etp+nc1tOxEvg2YlMAjKBJKkLuCTMf2wS+CKg6ZSaY/9aNmdlBXizezx2PTBHuyf8nUhDUEBxI7VtBMBwMeo0UojJAkYxslp0cm58+P6kn4cvMOSO9GcDAAA=", + "Killmail (structure)": "fit:killmail:117621358/efe9a3e74e6e0ef983846a82a234211090b94fd5", + "Killmail (ship)": "fit:killmail:117923593/4863ca35a23b480dc9feead5e87f2a9fbf1b1102", + "Buzzard (eft)": + "fit:eft:H4sIAAAAAAAA/4VSy3IaMRC86yvmA5Iq8C42Pq7XdkIVYAI4l1QOg1asp7wrbQaJ19dnJCBUcclJU+rumZ7Hr6dwPCJXX6BokRlKZz27BpZuZ/i3mpBmB0XYU0PIB5jFbyGxgZGaonVrWsnHyHrDFhtYeA7ahwj/H1eDyRR+BKxgTK8wNxvPSNZUkKrukLuKaWvU3DSkoRD94RiTjdTy0JmvL/Biq8BkayiRawcLjdYaVvGFQv8JtCFPTmJmPERdQuZoa7MmW0XlP0ilpmbsVgbGGKz+OJUq3dawh7duA2Xj8DOKns2WtDmhbYfanxqsSKdy09A0tCZxohYtNg18Y9ySP4hNIZN3DO9dzVilFCfKd+mINx16kin9NI3TUfDWeWrp3LQquBXprEFvNrDvq6fAcVpjV8t4SmIdyMP+rq9kHCvxIZTyQ5Z6meiVk+fqGVusBSnYi1cdy05N4PT4neNPkWfqlUk4qbs1SsvXKpm6qZupifFxxZqxi/76A3VTNosnQV7SyNhcsFW0eD6pK+dyY7Lc5FUtzmMpvA/WtMZ6eLcR6qul7LIRh7eanloydd0VuaR/UMNer5dHrQT9ewlyiYYPmUSP6U/A/D5Gd4PYZ1L0H8XZMEVCzPKUJUrUXxFh1SJBAwAA", }; -export const fullFits = [ - null, - { - name: "Tengu", - ship_type_id: 29984, - description: "", - items: [ - { flag: 5, quantity: 1, type_id: 35794 }, - { flag: 5, quantity: 1, type_id: 35794 }, - { flag: 5, quantity: 1, type_id: 35795 }, - { flag: 5, quantity: 3720, type_id: 24492 }, - { flag: 5, quantity: 396, type_id: 24492 }, - { flag: 5, quantity: 5472, type_id: 2679 }, - { flag: 5, quantity: 8, type_id: 30486 }, - { flag: 11, quantity: 1, type_id: 22291 }, - { flag: 12, quantity: 1, type_id: 22291 }, - { flag: 13, quantity: 1, type_id: 22291 }, - { flag: 19, quantity: 1, type_id: 41218 }, - { flag: 20, quantity: 1, type_id: 35790 }, - { flag: 21, quantity: 1, type_id: 2281 }, - { flag: 22, quantity: 1, type_id: 15766 }, - { flag: 23, quantity: 1, type_id: 19187 }, - { flag: 24, quantity: 1, type_id: 19187 }, - { flag: 25, quantity: 1, type_id: 35790 }, - { flag: 27, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 28, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 29, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 30, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 31, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 32, quantity: 1, type_id: 25715, charge: { type_id: 20308 } }, - { flag: 33, quantity: 1, type_id: 28756 }, - { flag: 92, quantity: 1, type_id: 31724 }, - { flag: 93, quantity: 1, type_id: 31824 }, - { flag: 94, quantity: 1, type_id: 31378 }, - { flag: 125, quantity: 1, type_id: 45626 }, - { flag: 126, quantity: 1, type_id: 45591 }, - { flag: 127, quantity: 1, type_id: 45601 }, - { flag: 128, quantity: 1, type_id: 45615 }, - ], - }, - { - name: "Legion", - ship_type_id: 29986, - description: "", - items: [ - { flag: 5, quantity: 1, type_id: 32014 }, - { flag: 5, quantity: 1, type_id: 33474 }, - { flag: 5, quantity: 2, type_id: 30832 }, - { flag: 5, quantity: 2, type_id: 30834 }, - { flag: 5, quantity: 6, type_id: 12826 }, - { flag: 11, quantity: 1, type_id: 3530 }, - { flag: 12, quantity: 1, type_id: 14072 }, - { flag: 13, quantity: 1, type_id: 14072 }, - { flag: 14, quantity: 1, type_id: 5839 }, - { flag: 15, quantity: 1, type_id: 2364 }, - { flag: 19, quantity: 1, type_id: 2024 }, - { flag: 20, quantity: 1, type_id: 3244 }, - { flag: 21, quantity: 1, type_id: 527 }, - { flag: 22, quantity: 1, type_id: 35656 }, - { flag: 27, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 28, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 29, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 30, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 31, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 32, quantity: 1, type_id: 3025, charge: { type_id: 253 } }, - { flag: 33, quantity: 1, type_id: 28756 }, - { flag: 34, quantity: 1, type_id: 11578 }, - { flag: 92, quantity: 1, type_id: 31055 }, - { flag: 93, quantity: 1, type_id: 31215 }, - { flag: 94, quantity: 1, type_id: 31071 }, - { flag: 125, quantity: 1, type_id: 45612 }, - { flag: 126, quantity: 1, type_id: 45586 }, - { flag: 127, quantity: 1, type_id: 45622 }, - { flag: 128, quantity: 1, type_id: 45598 }, - ], - }, - { - name: "Loki", +export const esiFits = { + Loki: { ship_type_id: 29990, + name: "C3 HAM", description: "", items: [ - { flag: 5, quantity: 1, type_id: 33700 }, - { flag: 5, quantity: 150, type_id: 28668 }, - { flag: 5, quantity: 16, type_id: 30486 }, - { flag: 5, quantity: 16, type_id: 30488 }, - { flag: 5, quantity: 330, type_id: 2679 }, - { flag: 5, quantity: 9000, type_id: 13856 }, - { flag: 5, quantity: 9000, type_id: 24488 }, { flag: 11, quantity: 1, type_id: 22291 }, { flag: 12, quantity: 1, type_id: 22291 }, + { flag: 125, quantity: 1, type_id: 45633 }, + { flag: 126, quantity: 1, type_id: 45595 }, + { flag: 127, quantity: 1, type_id: 45608 }, + { flag: 128, quantity: 1, type_id: 45621 }, { flag: 19, quantity: 1, type_id: 19203 }, { flag: 20, quantity: 1, type_id: 19289 }, { flag: 21, quantity: 1, type_id: 2281 }, @@ -136,67 +67,185 @@ export const fullFits = [ { flag: 23, quantity: 1, type_id: 14142 }, { flag: 24, quantity: 1, type_id: 41220 }, { flag: 25, quantity: 1, type_id: 14108 }, - { flag: 27, quantity: 1, type_id: 25715, charge: { type_id: 24488 } }, - { flag: 28, quantity: 1, type_id: 25715, charge: { type_id: 24488 } }, - { flag: 29, quantity: 1, type_id: 25715, charge: { type_id: 24488 } }, - { flag: 30, quantity: 1, type_id: 25715, charge: { type_id: 24488 } }, - { flag: 31, quantity: 1, type_id: 25715, charge: { type_id: 24488 } }, + { flag: 27, quantity: 1, type_id: 25715 }, + { flag: 28, quantity: 1, type_id: 25715 }, + { flag: 29, quantity: 1, type_id: 25715 }, + { flag: 30, quantity: 1, type_id: 25715 }, + { flag: 31, quantity: 1, type_id: 25715 }, { flag: 32, quantity: 1, type_id: 30836 }, { flag: 33, quantity: 1, type_id: 11578 }, - { flag: 34, quantity: 1, type_id: 28756, charge: { type_id: 30488 } }, + { flag: 34, quantity: 1, type_id: 28756 }, + { flag: 5, quantity: 1, type_id: 33700 }, + { flag: 5, quantity: 150, type_id: 28668 }, + { flag: 5, quantity: 16, type_id: 30486 }, + { flag: 5, quantity: 16, type_id: 30488 }, + { flag: 5, quantity: 330, type_id: 2679 }, + { flag: 5, quantity: 9000, type_id: 13856 }, + { flag: 5, quantity: 9000, type_id: 24488 }, { flag: 87, quantity: 8, type_id: 2456 }, { flag: 92, quantity: 1, type_id: 31748 }, { flag: 93, quantity: 1, type_id: 31760 }, { flag: 94, quantity: 1, type_id: 31588 }, - { flag: 125, quantity: 1, type_id: 45633 }, - { flag: 126, quantity: 1, type_id: 45595 }, - { flag: 127, quantity: 1, type_id: 45608 }, - { flag: 128, quantity: 1, type_id: 45621 }, ], }, +}; + +export const fullFits: EsfFit[] = [ + null, { + name: "Tengu", + shipTypeId: 29984, + description: "", + cargo: [ + { quantity: 1, typeId: 35794 }, + { quantity: 1, typeId: 35794 }, + { quantity: 1, typeId: 35795 }, + { quantity: 3720, typeId: 24492 }, + { quantity: 396, typeId: 24492 }, + { quantity: 5472, typeId: 2679 }, + { quantity: 8, typeId: 30486 }, + ], + modules: [ + { slot: { type: "Low", index: 1 }, typeId: 22291, state: "Active" }, + { slot: { type: "Low", index: 2 }, typeId: 22291, state: "Active" }, + { slot: { type: "Low", index: 3 }, typeId: 22291, state: "Active" }, + { slot: { type: "Medium", index: 1 }, typeId: 41218, state: "Active" }, + { slot: { type: "Medium", index: 2 }, typeId: 35790, state: "Active" }, + { slot: { type: "Medium", index: 3 }, typeId: 2281, state: "Active" }, + { slot: { type: "Medium", index: 4 }, typeId: 15766, state: "Active" }, + { slot: { type: "Medium", index: 5 }, typeId: 19187, state: "Active" }, + { slot: { type: "Medium", index: 6 }, typeId: 19187, state: "Active" }, + { slot: { type: "Medium", index: 7 }, typeId: 35790, state: "Active" }, + { slot: { type: "High", index: 1 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 2 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 3 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 4 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 5 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 6 }, typeId: 25715, state: "Active", charge: { typeId: 20308 } }, + { slot: { type: "High", index: 7 }, typeId: 28756, state: "Active" }, + { slot: { type: "Rig", index: 1 }, typeId: 31724, state: "Active" }, + { slot: { type: "Rig", index: 2 }, typeId: 31824, state: "Active" }, + { slot: { type: "Rig", index: 3 }, typeId: 31378, state: "Active" }, + { slot: { type: "SubSystem", index: 1 }, typeId: 45626, state: "Active" }, + { slot: { type: "SubSystem", index: 2 }, typeId: 45591, state: "Active" }, + { slot: { type: "SubSystem", index: 3 }, typeId: 45601, state: "Active" }, + { slot: { type: "SubSystem", index: 4 }, typeId: 45615, state: "Active" }, + ], + drones: [], + }, + { + name: "Legion", + shipTypeId: 29986, + description: "", + modules: [ + { slot: { type: "Low", index: 1 }, typeId: 3530, state: "Active" }, + { slot: { type: "Low", index: 2 }, typeId: 14072, state: "Active" }, + { slot: { type: "Low", index: 3 }, typeId: 14072, state: "Active" }, + { slot: { type: "Low", index: 4 }, typeId: 5839, state: "Active" }, + { slot: { type: "Low", index: 5 }, typeId: 2364, state: "Active" }, + { slot: { type: "Medium", index: 1 }, typeId: 2024, state: "Active" }, + { slot: { type: "Medium", index: 2 }, typeId: 3244, state: "Active" }, + { slot: { type: "Medium", index: 3 }, typeId: 527, state: "Active" }, + { slot: { type: "Medium", index: 4 }, typeId: 35656, state: "Active" }, + { slot: { type: "High", index: 1 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 2 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 3 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 4 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 5 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 6 }, typeId: 3025, state: "Active", charge: { typeId: 253 } }, + { slot: { type: "High", index: 7 }, typeId: 28756, state: "Active" }, + { slot: { type: "High", index: 8 }, typeId: 11578, state: "Active" }, + { slot: { type: "Rig", index: 1 }, typeId: 31055, state: "Active" }, + { slot: { type: "Rig", index: 2 }, typeId: 31215, state: "Active" }, + { slot: { type: "Rig", index: 3 }, typeId: 31071, state: "Active" }, + { slot: { type: "SubSystem", index: 1 }, typeId: 45612, state: "Active" }, + { slot: { type: "SubSystem", index: 2 }, typeId: 45586, state: "Active" }, + { slot: { type: "SubSystem", index: 3 }, typeId: 45622, state: "Active" }, + { slot: { type: "SubSystem", index: 4 }, typeId: 45598, state: "Active" }, + ], + drones: [], + cargo: [ + { quantity: 1, typeId: 32014 }, + { quantity: 1, typeId: 33474 }, + { quantity: 2, typeId: 30832 }, + { quantity: 2, typeId: 30834 }, + { quantity: 6, typeId: 12826 }, + ], + }, + { + name: "Loki", + description: "", + shipTypeId: 29990, + modules: [ + { slot: { type: "Low", index: 1 }, typeId: 22291, state: "Active" }, + { slot: { type: "Low", index: 2 }, typeId: 22291, state: "Active" }, + { slot: { type: "Medium", index: 1 }, typeId: 19203, state: "Active" }, + { slot: { type: "Medium", index: 2 }, typeId: 19289, state: "Active" }, + { slot: { type: "Medium", index: 3 }, typeId: 2281, state: "Active" }, + { slot: { type: "Medium", index: 4 }, typeId: 17500, state: "Active" }, + { slot: { type: "Medium", index: 5 }, typeId: 14142, state: "Active" }, + { slot: { type: "Medium", index: 6 }, typeId: 41220, state: "Active" }, + { slot: { type: "Medium", index: 7 }, typeId: 14108, state: "Active" }, + { slot: { type: "High", index: 1 }, typeId: 25715, state: "Active", charge: { typeId: 24488 } }, + { slot: { type: "High", index: 2 }, typeId: 25715, state: "Active", charge: { typeId: 24488 } }, + { slot: { type: "High", index: 3 }, typeId: 25715, state: "Active", charge: { typeId: 24488 } }, + { slot: { type: "High", index: 4 }, typeId: 25715, state: "Active", charge: { typeId: 24488 } }, + { slot: { type: "High", index: 5 }, typeId: 25715, state: "Active", charge: { typeId: 24488 } }, + { slot: { type: "High", index: 6 }, typeId: 30836, state: "Active" }, + { slot: { type: "High", index: 7 }, typeId: 11578, state: "Active" }, + { slot: { type: "High", index: 8 }, typeId: 28756, state: "Active", charge: { typeId: 30488 } }, + { slot: { type: "Rig", index: 1 }, typeId: 31748, state: "Active" }, + { slot: { type: "Rig", index: 2 }, typeId: 31760, state: "Active" }, + { slot: { type: "Rig", index: 3 }, typeId: 31588, state: "Active" }, + { slot: { type: "SubSystem", index: 1 }, typeId: 45633, state: "Active" }, + { slot: { type: "SubSystem", index: 2 }, typeId: 45595, state: "Active" }, + { slot: { type: "SubSystem", index: 3 }, typeId: 45608, state: "Active" }, + { slot: { type: "SubSystem", index: 4 }, typeId: 45621, state: "Active" }, + ], + drones: [{ typeId: 2456, states: { Active: 5, Passive: 3 } }], + cargo: [ + { quantity: 1, typeId: 33700 }, + { quantity: 150, typeId: 28668 }, + { quantity: 16, typeId: 30486 }, + { quantity: 16, typeId: 30488 }, + { quantity: 330, typeId: 2679 }, + { quantity: 9000, typeId: 13856 }, + { quantity: 9000, typeId: 24488 }, + ], + }, + { + shipTypeId: 35833, name: "Killmail 117621358", - ship_type_id: 35833, description: "", - items: [ - { flag: 5, type_id: 37821, quantity: 6 }, - { flag: 5, type_id: 37822, quantity: 7 }, - { flag: 5, type_id: 37823, quantity: 7 }, - { flag: 5, type_id: 37824, quantity: 7 }, - { flag: 5, type_id: 37843, quantity: 6102 }, - { flag: 5, type_id: 37844, quantity: 4249 }, - { flag: 5, type_id: 63195, quantity: 17200 }, - { flag: 11, type_id: 47362, quantity: 1 }, - { flag: 12, type_id: 47342, quantity: 1 }, - { flag: 13, type_id: 47362, quantity: 1 }, - { flag: 14, type_id: 47362, quantity: 1 }, - { flag: 19, type_id: 35944, quantity: 1 }, - { flag: 20, type_id: 47334, quantity: 1 }, - { flag: 21, type_id: 47366, quantity: 1 }, - { flag: 22, type_id: 47338, quantity: 1 }, - { flag: 23, type_id: 47338, quantity: 1 }, - { flag: 27, type_id: 47327, quantity: 1 }, - { flag: 28, type_id: 47323, quantity: 1 }, - { flag: 29, type_id: 47323, quantity: 1 }, - { flag: 30, type_id: 47323, quantity: 1 }, - { flag: 31, type_id: 47330, quantity: 1 }, - { flag: 32, type_id: 47330, quantity: 1 }, - { flag: 92, type_id: 37254, quantity: 1 }, - { flag: 93, type_id: 37258, quantity: 1 }, - { flag: 94, type_id: 37260, quantity: 1 }, - { flag: 158, type_id: 47035, quantity: 10 }, - { flag: 158, type_id: 47037, quantity: 10 }, - { flag: 158, type_id: 47119, quantity: 12 }, - { flag: 158, type_id: 47123, quantity: 6 }, - { flag: 158, type_id: 47127, quantity: 6 }, - { flag: 158, type_id: 47127, quantity: 6 }, - { flag: 158, type_id: 47136, quantity: 3 }, - { flag: 158, type_id: 47141, quantity: 2 }, - { flag: 158, type_id: 47141, quantity: 6 }, - { flag: 158, type_id: 47143, quantity: 9 }, - { flag: 164, type_id: 35894, quantity: 1 }, - { flag: 172, type_id: 4246, quantity: 14030 }, - { flag: 180, type_id: 56204, quantity: 1 }, + modules: [ + { slot: { type: "Low", index: 1 }, typeId: 47362, state: "Active" }, + { slot: { type: "Low", index: 2 }, typeId: 47342, state: "Active" }, + { slot: { type: "Low", index: 3 }, typeId: 47362, state: "Active" }, + { slot: { type: "Low", index: 4 }, typeId: 47362, state: "Active" }, + { slot: { type: "Medium", index: 1 }, typeId: 35944, state: "Active" }, + { slot: { type: "Medium", index: 2 }, typeId: 47334, state: "Active" }, + { slot: { type: "Medium", index: 3 }, typeId: 47366, state: "Active" }, + { slot: { type: "Medium", index: 4 }, typeId: 47338, state: "Active" }, + { slot: { type: "Medium", index: 5 }, typeId: 47338, state: "Active" }, + { slot: { type: "High", index: 1 }, typeId: 47327, state: "Active" }, + { slot: { type: "High", index: 2 }, typeId: 47323, state: "Active" }, + { slot: { type: "High", index: 3 }, typeId: 47323, state: "Active" }, + { slot: { type: "High", index: 4 }, typeId: 47323, state: "Active" }, + { slot: { type: "High", index: 5 }, typeId: 47330, state: "Active" }, + { slot: { type: "High", index: 6 }, typeId: 47330, state: "Active" }, + { slot: { type: "Rig", index: 1 }, typeId: 37254, state: "Active" }, + { slot: { type: "Rig", index: 2 }, typeId: 37258, state: "Active" }, + { slot: { type: "Rig", index: 3 }, typeId: 37260, state: "Active" }, + ], + drones: [], + cargo: [ + { typeId: 37821, quantity: 6 }, + { typeId: 37844, quantity: 4249 }, + { typeId: 37822, quantity: 7 }, + { typeId: 37824, quantity: 7 }, + { typeId: 37843, quantity: 6102 }, + { typeId: 37823, quantity: 7 }, + { typeId: 63195, quantity: 17200 }, ], }, ]; diff --git a/.storybook/helpers.tsx b/.storybook/helpers.tsx index b448f6c..f17605b 100644 --- a/.storybook/helpers.tsx +++ b/.storybook/helpers.tsx @@ -36,9 +36,11 @@ export const withDecoratorFull = (Story: StoryFn) => ( export const useFitSelection = (fit: EsfFit | null) => { const currentFit = useCurrentFit(); - const setFit = currentFit.setFit; + + const setFitRef = React.useRef(currentFit.setFit); + setFitRef.current = currentFit.setFit; React.useEffect(() => { - setFit(fit); - }, [setFit, fit]); + setFitRef.current(fit); + }, [fit]); }; diff --git a/package.json b/package.json index 489d7df..989c8c9 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ }, "peerDependencies": { "@eveshipfit/data": "^9", - "@eveshipfit/dogma-engine": "^4", + "@eveshipfit/dogma-engine": "^5", "react": "^18", "react-dom": "^18" }, diff --git a/src/components/CalculationDetail/CalculationDetail.tsx b/src/components/CalculationDetail/CalculationDetail.tsx index 08b9d8b..5c9116f 100644 --- a/src/components/CalculationDetail/CalculationDetail.tsx +++ b/src/components/CalculationDetail/CalculationDetail.tsx @@ -3,7 +3,8 @@ import React from "react"; import { Icon } from "@/components/Icon"; import { useEveData } from "@/providers/EveDataProvider"; -import { StatisticsItemAttribute, StatisticsItemAttributeEffect, useStatistics } from "@/providers/StatisticsProvider"; +import { useStatistics } from "@/providers/StatisticsProvider"; +import { CalculationItemAttribute, CalculationItemAttributeEffect } from "@/providers/DogmaEngineProvider"; import styles from "./CalculationDetail.module.css"; @@ -42,7 +43,7 @@ function stateToInteger(state: string): number { } } -const Effect = (props: { effect: StatisticsItemAttributeEffect }) => { +const Effect = (props: { effect: CalculationItemAttributeEffect }) => { const eveData = useEveData(); const statistics = useStatistics(); @@ -123,7 +124,7 @@ const Effect = (props: { effect: StatisticsItemAttributeEffect }) => { ); }; -const CalculationDetailMeta = (props: { attributeId: number; attribute: StatisticsItemAttribute }) => { +const CalculationDetailMeta = (props: { attributeId: number; attribute: CalculationItemAttribute }) => { const [expanded, setExpanded] = React.useState(false); const eveData = useEveData(); @@ -175,7 +176,7 @@ export const CalculationDetail = (props: { const statistics = useStatistics(); if (statistics === null) return <>; - let attributes: [number, StatisticsItemAttribute][] = []; + let attributes: [number, CalculationItemAttribute][] = []; if (props.source === "Ship") { attributes = [...(statistics.hull.attributes.entries() ?? [])]; diff --git a/src/components/DroneBay/DroneBay.tsx b/src/components/DroneBay/DroneBay.tsx index c0b3267..38d125e 100644 --- a/src/components/DroneBay/DroneBay.tsx +++ b/src/components/DroneBay/DroneBay.tsx @@ -4,11 +4,20 @@ import React from "react"; import { CharAttribute, ShipAttribute } from "@/components/ShipAttribute"; import { useFitManager } from "@/providers/FitManagerProvider"; import { useEveData } from "@/providers/EveDataProvider"; -import { StatisticsItem, useStatistics } from "@/providers/StatisticsProvider"; +import { useStatistics } from "@/providers/StatisticsProvider"; +import { CalculationItem } from "@/providers/DogmaEngineProvider"; import styles from "./DroneBay.module.css"; -const DroneBayEntrySelected = ({ drone, index, isOpen }: { drone: StatisticsItem; index: number; isOpen: boolean }) => { +const DroneBayEntrySelected = ({ + drone, + index, + isOpen, +}: { + drone: CalculationItem; + index: number; + isOpen: boolean; +}) => { const fitManager = useFitManager(); const onClick = React.useCallback(() => { @@ -29,7 +38,7 @@ const DroneBayEntrySelected = ({ drone, index, isOpen }: { drone: StatisticsItem ); }; -const DroneBayEntry = ({ name, drones }: { name: string; drones: StatisticsItem[] }) => { +const DroneBayEntry = ({ name, drones }: { name: string; drones: CalculationItem[] }) => { const eveData = useEveData(); const statistics = useStatistics(); const fitManager = useFitManager(); @@ -93,8 +102,8 @@ export const DroneBay = () => { if (eveData === null || statistics === null) return <>; /* Group drones by type_id */ - const dronesGrouped: Record = {}; - for (const drone of statistics.items.filter((item) => item.flag == 87)) { + const dronesGrouped: Record = {}; + for (const drone of statistics.items.filter((item) => item.slot.type == "DroneBay")) { const name = eveData.typeIDs?.[drone.type_id].name ?? ""; if (dronesGrouped[name] === undefined) { diff --git a/src/components/FitButtonBar/ClipboardButton.tsx b/src/components/FitButtonBar/ClipboardButton.tsx index c43c3ae..218a916 100644 --- a/src/components/FitButtonBar/ClipboardButton.tsx +++ b/src/components/FitButtonBar/ClipboardButton.tsx @@ -5,6 +5,7 @@ import { ModalDialog } from "@/components/ModalDialog"; import { useClipboard } from "@/hooks/Clipboard"; import { useExportEft } from "@/hooks/ExportEft"; import { useImportEft } from "@/hooks/ImportEft"; +import { useImportEsiFitting } from "@/hooks/ImportEsiFitting"; import { EsfFit } from "@/providers/CurrentFitProvider"; import { useFitManager } from "@/providers/FitManagerProvider"; @@ -14,6 +15,7 @@ export const ClipboardButton = () => { const fitManager = useFitManager(); const exportEft = useExportEft(); const importEft = useImportEft(); + const importEsiFitting = useImportEsiFitting(); const { copy, copied } = useClipboard(); const [isPopupOpen, setIsPopupOpen] = React.useState(false); @@ -42,7 +44,7 @@ export const ClipboardButton = () => { let fit: EsfFit | undefined | null; if (fitString.startsWith("{")) { - fit = JSON.parse(fitString); + fit = importEsiFitting(JSON.parse(fitString)); } else { try { fit = importEft(fitString); @@ -66,7 +68,7 @@ export const ClipboardButton = () => { setIsPasteOpen(false); setIsPopupOpen(false); - }, [fitManager, importEft]); + }, [fitManager, importEft, importEsiFitting]); return ( <> diff --git a/src/components/HardwareListing/HardwareListing.tsx b/src/components/HardwareListing/HardwareListing.tsx index 88e6108..5a0d112 100644 --- a/src/components/HardwareListing/HardwareListing.tsx +++ b/src/components/HardwareListing/HardwareListing.tsx @@ -4,9 +4,10 @@ import React from "react"; import { defaultDataUrl } from "@/settings"; import { Icon } from "@/components/Icon"; import { TreeListing, TreeHeader, TreeLeaf } from "@/components/TreeListing"; -import { StatisticsSlotType, useStatistics } from "@/providers/StatisticsProvider"; +import { useStatistics } from "@/providers/StatisticsProvider"; import { useFitManager } from "@/providers/FitManagerProvider"; import { useEveData } from "@/providers/EveDataProvider"; +import { CalculationSlotType } from "@/providers/DogmaEngineProvider"; import styles from "./HardwareListing.module.css"; @@ -21,7 +22,7 @@ interface ListingItem { name: string; meta: number; typeId: number; - slotType: StatisticsSlotType | "droneBay" | "charge"; + slotType: CalculationSlotType; } interface ListingGroup { @@ -43,14 +44,14 @@ interface Filter { const OnItemDragStart = ( typeId: number, - slotType: StatisticsSlotType | "droneBay" | "charge", + slotType: CalculationSlotType, ): ((e: React.DragEvent) => void) => { return (e: React.DragEvent) => { const img = new Image(); img.src = `https://images.evetech.net/types/${typeId}/icon?size=64`; e.dataTransfer.setDragImage(img, 32, 32); - e.dataTransfer.setData("application/type_id", typeId.toString()); - e.dataTransfer.setData("application/slot_type", slotType); + e.dataTransfer.setData("application/esf-type-id", typeId.toString()); + e.dataTransfer.setData("application/esf-slot-type", slotType); }; }; @@ -193,35 +194,35 @@ export const HardwareListing = () => { if (module.marketGroupID === undefined) continue; if (!module.published) continue; - let slotType: StatisticsSlotType | "droneBay" | "charge" | undefined; + let slotType: CalculationSlotType | undefined; if (module.categoryID !== 8) { slotType = eveData.typeDogma[typeId]?.dogmaEffects .map((effect) => { switch (effect.effectID) { case eveData.effectMapping.loPower: - return "lowslot"; + return "Low"; case eveData.effectMapping.medPower: - return "medslot"; + return "Medium"; case eveData.effectMapping.hiPower: - return "hislot"; + return "High"; case eveData.effectMapping.rigSlot: - return "rig"; + return "Rig"; case eveData.effectMapping.subSystem: - return "subsystem"; + return "SubSystem"; } }) .filter((slot) => slot !== undefined)[0]; if (module.categoryID === 18) { - slotType = "droneBay"; + slotType = "DroneBay"; } if (slotType === undefined) continue; if (filter.lowslot || filter.medslot || filter.hislot || filter.rig_subsystem || filter.drone) { - if (slotType === "lowslot" && !filter.lowslot) continue; - if (slotType === "medslot" && !filter.medslot) continue; - if (slotType === "hislot" && !filter.hislot) continue; - if ((slotType === "rig" || slotType === "subsystem") && !filter.rig_subsystem) continue; + if (slotType === "Low" && !filter.lowslot) continue; + if (slotType === "Medium" && !filter.medslot) continue; + if (slotType === "High" && !filter.hislot) continue; + if ((slotType === "Rig" || slotType === "SubSystem") && !filter.rig_subsystem) continue; if (module.categoryID === 18 && !filter.drone) continue; } } else { @@ -236,13 +237,13 @@ export const HardwareListing = () => { for (const chargeGroupID of filter.moduleWithCharge.chargeGroupIDs) { if (module.groupID !== chargeGroupID) continue; - slotType = "charge"; + slotType = "Charge"; break; } if (slotType === undefined) continue; } else { - slotType = "charge"; + slotType = "Charge"; } } diff --git a/src/components/HullListing/HullListing.tsx b/src/components/HullListing/HullListing.tsx index 5bdbc4e..1470a4a 100644 --- a/src/components/HullListing/HullListing.tsx +++ b/src/components/HullListing/HullListing.tsx @@ -72,7 +72,7 @@ const Hull = (props: { typeId: number; entry: ListingHull }) => { return ( fitManager.setFit(fit.fit)} @@ -166,13 +166,13 @@ export const HullListing = () => { const localFitsGrouped = React.useMemo(() => { const grouped: Record = {}; for (const fit of localFits.fittings) { - if (fit.ship_type_id === undefined) continue; + if (fit.shipTypeId === undefined) continue; - if (grouped[fit.ship_type_id] === undefined) { - grouped[fit.ship_type_id] = []; + if (grouped[fit.shipTypeId] === undefined) { + grouped[fit.shipTypeId] = []; } - grouped[fit.ship_type_id].push({ + grouped[fit.shipTypeId].push({ origin: "local", fit, }); @@ -186,13 +186,13 @@ export const HullListing = () => { const grouped: Record = {}; for (const fit of characterFittings) { - if (fit.ship_type_id === undefined) continue; + if (fit.shipTypeId === undefined) continue; - if (grouped[fit.ship_type_id] === undefined) { - grouped[fit.ship_type_id] = []; + if (grouped[fit.shipTypeId] === undefined) { + grouped[fit.shipTypeId] = []; } - grouped[fit.ship_type_id].push({ + grouped[fit.shipTypeId].push({ origin: "character", fit, }); @@ -213,7 +213,7 @@ export const HullListing = () => { if (hull.marketGroupID === undefined) continue; if (!hull.published) continue; - if (filter.currentHull && currentFit.fit?.ship_type_id !== parseInt(typeId)) continue; + if (filter.currentHull && currentFit.fit?.shipTypeId !== parseInt(typeId)) continue; const fits: ListingFit[] = []; if (anyFilter) { @@ -221,7 +221,7 @@ export const HullListing = () => { if (filter.characterFits && Object.keys(characterFitsGrouped).includes(typeId)) fits.push(...characterFitsGrouped[typeId]); if (fits.length == 0) { - if (!filter.currentHull || currentFit.fit?.ship_type_id !== parseInt(typeId)) continue; + if (!filter.currentHull || currentFit.fit?.shipTypeId !== parseInt(typeId)) continue; } } else { if (Object.keys(localFitsGrouped).includes(typeId)) fits.push(...localFitsGrouped[typeId]); diff --git a/src/components/ShipFit/Hull.tsx b/src/components/ShipFit/Hull.tsx index 9f88127..8b792b1 100644 --- a/src/components/ShipFit/Hull.tsx +++ b/src/components/ShipFit/Hull.tsx @@ -14,7 +14,7 @@ export const Hull = () => { return <>; } - const shipTypeId = currentFit.fit.ship_type_id; + const shipTypeId = currentFit.fit.shipTypeId; if (shipTypeId === undefined) { return <>; } diff --git a/src/components/ShipFit/HullDraggable.tsx b/src/components/ShipFit/HullDraggable.tsx index 14b35aa..9a11960 100644 --- a/src/components/ShipFit/HullDraggable.tsx +++ b/src/components/ShipFit/HullDraggable.tsx @@ -1,7 +1,7 @@ import React from "react"; import { useFitManager } from "@/providers/FitManagerProvider"; -import { StatisticsSlotType } from "@/providers/StatisticsProvider"; +import { CalculationSlotType } from "@/providers/DogmaEngineProvider"; import styles from "./ShipFit.module.css"; @@ -21,11 +21,11 @@ export const HullDraggable = () => { return Number.isInteger(num) ? num : undefined; }; - const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/type_id")); - const draggedSlotId: number | undefined = parseNumber(e.dataTransfer.getData("application/slot_id")); - const draggedSlotType: StatisticsSlotType | "droneBay" | "charge" = e.dataTransfer.getData( - "application/slot_type", - ) as StatisticsSlotType | "droneBay" | "charge"; + const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/esf-type-id")); + const draggedSlotId: number | undefined = parseNumber(e.dataTransfer.getData("application/esf-slot-index")); + const draggedSlotType: CalculationSlotType = e.dataTransfer.getData( + "application/esf-slot-type", + ) as CalculationSlotType; if (draggedTypeId === undefined) { return; diff --git a/src/components/ShipFit/RadialMenu.tsx b/src/components/ShipFit/RadialMenu.tsx index 3084039..4f213e3 100644 --- a/src/components/ShipFit/RadialMenu.tsx +++ b/src/components/ShipFit/RadialMenu.tsx @@ -3,19 +3,19 @@ import React from "react"; import styles from "./ShipFit.module.css"; const highlightSettings = { - lowslot: { + Low: { width: 12, height: 3, x: 0, y: 9, }, - medslot: { + Medium: { width: 3, height: 12, x: 9, y: 0, }, - hislot: { + High: { width: 12, height: 3, x: 0, @@ -23,7 +23,7 @@ const highlightSettings = { }, }; -export const RadialMenu = (props: { type: "lowslot" | "medslot" | "hislot" }) => { +export const RadialMenu = (props: { type: "Low" | "Medium" | "High" }) => { const highlight = highlightSettings[props.type]; return ( diff --git a/src/components/ShipFit/ShipFit.tsx b/src/components/ShipFit/ShipFit.tsx index d0fc8c7..46c62d3 100644 --- a/src/components/ShipFit/ShipFit.tsx +++ b/src/components/ShipFit/ShipFit.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { Icon } from "@/components/Icon"; import { useEveData } from "@/providers/EveDataProvider"; -import { useStatistics } from "@/providers/StatisticsProvider"; +import { StatisticsSlots, useStatistics } from "@/providers/StatisticsProvider"; import { FitLink } from "./FitLink"; import { Hull } from "./Hull"; @@ -26,14 +26,14 @@ export const ShipFit = (props: { withStats?: boolean }) => { if (eveData === null) return <>; - const slots = statistics?.slots ?? { - hislot: 0, - medslot: 0, - lowslot: 0, - rig: 0, - subsystem: 0, - turret: 0, - launcher: 0, + const slots: StatisticsSlots = statistics?.slots ?? { + High: 0, + Medium: 0, + Low: 0, + Rig: 0, + SubSystem: 0, + Turret: 0, + Launcher: 0, }; let launcherSlotsUsed = @@ -65,7 +65,7 @@ export const ShipFit = (props: { withStats?: boolean }) => { - {Array.from({ length: slots.turret }, (_, i) => { + {Array.from({ length: slots.Turret }, (_, i) => { turretSlotsUsed--; return ( @@ -85,7 +85,7 @@ export const ShipFit = (props: { withStats?: boolean }) => { - {Array.from({ length: slots.launcher }, (_, i) => { + {Array.from({ length: slots.Launcher }, (_, i) => { launcherSlotsUsed--; return ( @@ -119,113 +119,113 @@ export const ShipFit = (props: { withStats?: boolean }) => { )} - + - = 1} main /> + = 1} main /> - = 2} /> + = 2} /> - = 3} /> + = 3} /> - = 4} /> + = 4} /> - = 5} /> + = 5} /> - = 6} /> + = 6} /> - = 7} /> + = 7} /> - = 8} /> + = 8} /> - + - = 1} /> + = 1} /> - = 2} /> + = 2} /> - = 3} /> + = 3} /> - = 4} /> + = 4} /> - = 5} /> + = 5} /> - = 6} /> + = 6} /> - = 7} /> + = 7} /> - = 8} /> + = 8} /> - + - = 1} /> + = 1} /> - = 2} /> + = 2} /> - = 3} /> + = 3} /> - = 4} /> + = 4} /> - = 5} /> + = 5} /> - = 6} /> + = 6} /> - = 7} /> + = 7} /> - = 8} /> + = 8} /> - = 1} /> + = 1} /> - = 2} /> + = 2} /> - = 3} /> + = 3} /> - = 1} /> + = 1} /> - = 2} /> + = 2} /> - = 3} /> + = 3} /> - = 4} /> + = 4} /> diff --git a/src/components/ShipFit/Slot.tsx b/src/components/ShipFit/Slot.tsx index 3735b9e..739c38b 100644 --- a/src/components/ShipFit/Slot.tsx +++ b/src/components/ShipFit/Slot.tsx @@ -4,57 +4,47 @@ import { Icon, IconName } from "@/components/Icon"; import { useEveData } from "@/providers/EveDataProvider"; import { useStatistics } from "@/providers/StatisticsProvider"; import { useFitManager } from "@/providers/FitManagerProvider"; -import { State } from "@/providers/CurrentFitProvider"; +import { CalculationSlot } from "@/providers/DogmaEngineProvider"; +import { EsfSlot, EsfSlotType, EsfState } from "@/providers/CurrentFitProvider"; import styles from "./ShipFit.module.css"; -const esiFlagMapping: Record = { - lowslot: [11, 12, 13, 14, 15, 16, 17, 18], - medslot: [19, 20, 21, 22, 23, 24, 25, 26], - hislot: [27, 28, 29, 30, 31, 32, 33, 34], - rig: [92, 93, 94], - subsystem: [125, 126, 127, 128], -}; - -const stateRotation: Record = { +const stateRotation: Record = { Passive: ["Passive"], Online: ["Passive", "Online"], Active: ["Passive", "Online", "Active"], Overload: ["Passive", "Online", "Active", "Overload"], }; -export const Slot = (props: { type: string; index: number; fittable: boolean; main?: boolean }) => { +export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolean; main?: boolean }) => { const eveData = useEveData(); const statistics = useStatistics(); const fitManager = useFitManager(); - const esiFlagType = props.type; - const esiFlag = esiFlagMapping[esiFlagType][props.index - 1]; - - const esiItem = statistics?.items.find((item) => item.flag == esiFlag); - const active = esiItem?.max_state !== "Passive" && esiItem?.max_state !== "Online"; + const module = statistics?.items.find((item) => item.slot.type === props.type && item.slot.index === props.index); + const active = module?.max_state !== "Passive" && module?.max_state !== "Online"; const offlineState = React.useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if (esiItem === undefined) return; + if (module === undefined) return; - if (esiItem.state === "Passive") { - fitManager.setModuleState(esiItem.flag, "Online"); + if (module.state === "Passive") { + fitManager.setModuleState(module.slot as EsfSlot, "Online"); } else { - fitManager.setModuleState(esiItem.flag, "Passive"); + fitManager.setModuleState(module.slot as EsfSlot, "Passive"); } }, - [fitManager, esiItem], + [fitManager, module], ); const cycleState = React.useCallback( (e: React.MouseEvent) => { - if (esiItem === undefined) return; + if (module === undefined) return; - const states = stateRotation[esiItem.max_state]; - const stateIndex = states.indexOf(esiItem.state); + const states = stateRotation[module.max_state]; + const stateIndex = states.indexOf(module.state as EsfState); let newState; if (e.shiftKey) { @@ -63,41 +53,41 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma newState = states[(stateIndex + 1) % states.length]; } - fitManager.setModuleState(esiItem.flag, newState); + fitManager.setModuleState(module.slot as EsfSlot, newState); }, - [fitManager, esiItem], + [fitManager, module], ); const unfitModule = React.useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if (esiItem === undefined) return; + if (module === undefined) return; - fitManager.removeModule(esiItem.flag); + fitManager.removeModule(module.slot as EsfSlot); }, - [fitManager, esiItem], + [fitManager, module], ); const unfitCharge = React.useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - if (esiItem === undefined) return; + if (module === undefined) return; - fitManager.removeCharge(esiItem.flag); + fitManager.removeCharge(module.slot as EsfSlot); }, - [fitManager, esiItem], + [fitManager, module], ); const onDragStart = React.useCallback( (e: React.DragEvent) => { - if (esiItem === undefined) return; + if (module === undefined) return; - e.dataTransfer.setData("application/type_id", esiItem.type_id.toString()); - e.dataTransfer.setData("application/slot_id", esiFlag.toString()); - e.dataTransfer.setData("application/slot_type", esiFlagType); + e.dataTransfer.setData("application/esf-type-id", module.type_id.toString()); + e.dataTransfer.setData("application/esf-slot-type", module.slot.type); + e.dataTransfer.setData("application/esf-slot-index", module.slot.index?.toString() ?? ""); }, - [esiItem, esiFlag, esiFlagType], + [module], ); const onDragOver = React.useCallback((e: React.DragEvent) => { @@ -113,32 +103,39 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma return Number.isInteger(num) ? num : undefined; }; - const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/type_id")); - const draggedSlotId: number | undefined = parseNumber(e.dataTransfer.getData("application/slot_id")); - const draggedSlotType: string = e.dataTransfer.getData("application/slot_type"); + const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/esf-type-id")); + const draggedSlotIndex: CalculationSlot["index"] = parseNumber( + e.dataTransfer.getData("application/esf-slot-index"), + ); + const draggedSlotType: CalculationSlot["type"] = e.dataTransfer.getData( + "application/esf-slot-type", + ) as CalculationSlot["type"]; - if (draggedTypeId === undefined) { + if (draggedTypeId === undefined || draggedSlotType === "DroneBay") { return; } - if (draggedSlotType === "charge") { - fitManager.setCharge(esiFlag, draggedTypeId); + if (draggedSlotType === "Charge") { + fitManager.setCharge({ type: props.type, index: props.index }, draggedTypeId); return; } - const isValidSlotGroup = draggedSlotType === esiFlagType; + const isValidSlotGroup = draggedSlotType === props.type; if (!isValidSlotGroup) { return; } - const isDraggedFromAnotherSlot = draggedSlotId !== undefined; + const isDraggedFromAnotherSlot = draggedSlotIndex !== undefined; if (isDraggedFromAnotherSlot) { - fitManager.swapModule(esiFlag, draggedSlotId); + fitManager.swapModule( + { type: props.type, index: props.index }, + { type: draggedSlotType, index: draggedSlotIndex }, + ); } else { - fitManager.setModule(esiFlag, draggedTypeId); + fitManager.setModule({ type: props.type, index: props.index }, draggedTypeId); } }, - [fitManager, esiFlag, esiFlagType], + [fitManager, props], ); if (eveData === null || statistics === null) return <>; @@ -202,14 +199,14 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma preserveAspectRatio="xMidYMin slice" > - {props.fittable && esiItem && active && } - {props.fittable && esiItem && !active && } + {props.fittable && module !== undefined && active && } + {props.fittable && module !== undefined && !active && } ); /* Not fittable and nothing fitted; no need to render the slot. */ - if (esiItem === undefined && !props.fittable) { + if (module === undefined && !props.fittable) { return (
@@ -219,12 +216,12 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma ); } - if (esiItem !== undefined && eveData !== null) { - if (esiItem.charge !== undefined) { + if (module !== undefined && eveData !== null) { + if (module.charge !== undefined) { item = ( @@ -232,8 +229,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma } else { item = ( @@ -244,19 +241,19 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma let icon: IconName | undefined; switch (props.type) { - case "lowslot": + case "Low": icon = "fitting-lowslot"; break; - case "medslot": + case "Medium": icon = "fitting-medslot"; break; - case "hislot": + case "High": icon = "fitting-hislot"; break; - case "rig": + case "Rig": icon = "fitting-rig-subsystem"; break; } @@ -266,16 +263,16 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma } } - const state = esiItem?.state === "Passive" && esiItem?.max_state !== "Passive" ? "Offline" : esiItem?.state; + const state = module?.state === "Passive" && module?.max_state !== "Passive" ? "Offline" : module?.state; return ( -
+
{svg}
{item}
- {esiItem?.charge !== undefined && ( + {module?.charge !== undefined && ( Remove Charge @@ -285,7 +282,7 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma Unfit Module - {esiItem?.max_state !== "Passive" && ( + {module?.max_state !== "Passive" && ( Put Offline diff --git a/src/components/ShipFitExtended/ShipFitExtended.tsx b/src/components/ShipFitExtended/ShipFitExtended.tsx index 0cc380c..5b1e12b 100644 --- a/src/components/ShipFitExtended/ShipFitExtended.tsx +++ b/src/components/ShipFitExtended/ShipFitExtended.tsx @@ -35,7 +35,7 @@ const ShipDroneBay = () => { if (eveData === null) return <>; - const isStructure = eveData.typeIDs[currentFit.fit?.ship_type_id ?? 0]?.categoryID === 65; + const isStructure = eveData.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65; return ( <> diff --git a/src/components/ShipStatistics/ShipStatistics.tsx b/src/components/ShipStatistics/ShipStatistics.tsx index eb8329e..0ab4ae6 100644 --- a/src/components/ShipStatistics/ShipStatistics.tsx +++ b/src/components/ShipStatistics/ShipStatistics.tsx @@ -22,7 +22,7 @@ export const ShipStatistics = () => { const statistics = useStatistics(); let capacitorState = "Stable"; - const isStructure = eveData?.typeIDs[currentFit.fit?.ship_type_id ?? 0]?.categoryID === 65; + const isStructure = eveData?.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65; const attributeId = eveData?.attributeMapping.capacitorDepletesIn ?? 0; const capacitorDepletesIn = statistics?.hull.attributes.get(attributeId)?.value; diff --git a/src/hooks/ExportEft/ExportEft.tsx b/src/hooks/ExportEft/ExportEft.tsx index f040bab..cbd01a1 100644 --- a/src/hooks/ExportEft/ExportEft.tsx +++ b/src/hooks/ExportEft/ExportEft.tsx @@ -1,27 +1,16 @@ import React from "react"; -import { useCurrentFit } from "@/providers/CurrentFitProvider"; +import { EsfSlotType, useCurrentFit } from "@/providers/CurrentFitProvider"; import { useEveData } from "@/providers/EveDataProvider"; import { useStatistics } from "@/providers/StatisticsProvider"; -/** Mapping between slot types and ESI flags (for first slot in the type). */ -const esiFlagMapping: Record<"hislot" | "medslot" | "lowslot" | "subsystem" | "rig" | "droneBay", number[]> = { - lowslot: [11, 12, 13, 14, 15, 16, 17, 18], - medslot: [19, 20, 21, 22, 23, 24, 25, 26], - hislot: [27, 28, 29, 30, 31, 32, 33, 34], - rig: [92, 93, 94], - subsystem: [125, 126, 127, 128], - droneBay: [87], -}; - /** Mapping between slot-type and the EFT string name. */ -const slotToEft: Record<"hislot" | "medslot" | "lowslot" | "subsystem" | "rig" | "droneBay", string> = { - lowslot: "Low Slot", - medslot: "Mid Slot", - hislot: "High Slot", - rig: "Rig Slot", - subsystem: "Subsystem Slot", - droneBay: "Drone Bay", +const slotToEft: Record = { + Low: "Low Slot", + Medium: "Med Slot", + High: "High Slot", + Rig: "Rig Slot", + SubSystem: "Subsystem Slot", }; /** @@ -39,55 +28,49 @@ export function useExportEft() { let eft = ""; - const shipType = eveData.typeIDs[fit.ship_type_id]; + const shipType = eveData.typeIDs[fit.shipTypeId]; if (shipType === undefined) return null; eft += `[${shipType.name}, ${fit.name}]\n`; - for (const slotType of Object.keys(esiFlagMapping) as ( - | "hislot" - | "medslot" - | "lowslot" - | "subsystem" - | "rig" - | "droneBay" - )[]) { - let index = 1; - - for (const flag of esiFlagMapping[slotType]) { - if (slotType !== "droneBay" && index > statistics.slots[slotType]) break; - index += 1; - - const modules = fit.items.filter((item) => item.flag === flag); - if (modules === undefined || modules.length === 0) { + for (const slotType of ["High", "Medium", "Low", "Rig", "SubSystem"] as EsfSlotType[]) { + for (let i = 1; i <= statistics.slots[slotType]; i++) { + const module = fit.modules.find((item) => item.slot.type === slotType && item.slot.index === i); + if (module === undefined) { eft += "[Empty " + slotToEft[slotType] + "]\n"; continue; } - for (const module of modules) { - const moduleType = eveData.typeIDs[module.type_id]; - if (moduleType === undefined) { - eft += "[Empty " + slotToEft[slotType] + "]\n"; - continue; - } + const moduleType = eveData.typeIDs[module.typeId]; + if (moduleType === undefined) { + eft += "[Empty " + slotToEft[slotType] + "]\n"; + continue; + } - eft += moduleType.name; - if (module.quantity > 1) { - eft += ` x${module.quantity}`; + eft += moduleType.name; + if (module.charge !== undefined) { + const chargeType = eveData.typeIDs[module.charge.typeId]; + if (chargeType !== undefined) { + eft += `, ${chargeType.name}`; } - if (module.charge !== undefined) { - const chargeType = eveData.typeIDs[module.charge.type_id]; - if (chargeType !== undefined) { - eft += `, ${chargeType.name}`; - } - } - eft += "\n"; } + eft += "\n"; } eft += "\n"; } + for (const drone of fit.drones) { + const droneType = eveData.typeIDs[drone.typeId]; + if (droneType === undefined) continue; + + eft += droneType.name; + if (drone.states.Active > 1) { + eft += ` x${drone.states.Active + drone.states.Passive}`; + } + eft += "\n"; + } + return eft; }; } diff --git a/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx b/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx index 4c7a36d..0abb028 100644 --- a/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx +++ b/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx @@ -19,13 +19,19 @@ async function compress(str: string): Promise { } async function encodeFit(fit: EsfFit): Promise { - let result = `${fit.ship_type_id},${fit.name},${fit.description}\n`; + let result = `ship,${fit.shipTypeId},${fit.name},${fit.description}\n`; - for (const item of fit.items) { - result += `${item.flag},${item.type_id},${item.quantity},${item.charge?.type_id ?? ""},${item.state ?? ""}\n`; + for (const module of fit.modules) { + result += `module,${module.slot.type},${module.slot.index},${module.typeId},${module.state},${module.charge?.typeId ?? ""}\n`; + } + for (const drone of fit.drones) { + result += `drone,${drone.typeId},${drone.states.Active},${drone.states.Passive}\n`; + } + for (const cargo of fit.cargo) { + result += `cargo,${cargo.typeId},${cargo.quantity}\n`; } - return "v2:" + (await compress(result)); + return "v3:" + (await compress(result)); } /** diff --git a/src/hooks/ImportEft/ImportEft.tsx b/src/hooks/ImportEft/ImportEft.tsx index f5b2282..259679e 100644 --- a/src/hooks/ImportEft/ImportEft.tsx +++ b/src/hooks/ImportEft/ImportEft.tsx @@ -1,27 +1,18 @@ import React from "react"; -import { EsfFit } from "@/providers/CurrentFitProvider"; +import { EsfFit, EsfSlotType } from "@/providers/CurrentFitProvider"; import { useEveData } from "@/providers/EveDataProvider"; -/** Mapping between slot types and ESI flags (for first slot in the type). */ -const esiFlagMapping: Record = { - lowslot: [11, 12, 13, 14, 15, 16, 17, 18], - medslot: [19, 20, 21, 22, 23, 24, 25, 26], - hislot: [27, 28, 29, 30, 31, 32, 33, 34], - rig: [92, 93, 94], - subsystem: [125, 126, 127, 128], -}; - /** Mapping between dogma effect IDs and slot types. */ -const effectIdMapping: Record = { - 11: "lowslot", - 13: "medslot", - 12: "hislot", - 2663: "rig", - 3772: "subsystem", +const effectIdMapping: Record = { + 11: "Low", + 13: "Medium", + 12: "High", + 2663: "Rig", + 3772: "SubSystem", }; -const attributeIdMapping: Record = { - 1272: "droneBay", +const attributeIdMapping: Record = { + 1272: "DroneBay", }; /** @@ -34,9 +25,7 @@ export function useImportEft() { if (eveData === null) return null; function lookupTypeByName(name: string): number | undefined { - if (eveData === null) return undefined; - - for (const typeId in eveData.typeIDs) { + for (const typeId in eveData?.typeIDs) { const type = eveData.typeIDs[typeId]; if (type.name === name) { @@ -48,10 +37,12 @@ export function useImportEft() { } const fit: EsfFit = { + shipTypeId: 0, name: "EFT Import", description: "", - ship_type_id: 0, - items: [], + modules: [], + drones: [], + cargo: [], }; const lines = eft.trim().split("\n"); @@ -63,19 +54,18 @@ export function useImportEft() { const shipTypeId = lookupTypeByName(shipType); if (shipTypeId === undefined) throw new Error(`Unknown ship '${shipType}'.`); - fit.ship_type_id = shipTypeId; + fit.shipTypeId = shipTypeId; fit.name = lines[0].split(",")[1].slice(0, -1).trim(); - const slotIndex: Record = { - lowslot: 0, - medslot: 0, - hislot: 0, - rig: 0, - subsystem: 0, - droneBay: 0, + const slotIndex: Record = { + Low: 1, + Medium: 1, + High: 1, + Rig: 1, + SubSystem: 1, }; - let lastSlotType = ""; + let lastSlotType: EsfSlotType | "DroneBay" | undefined = undefined; for (let i = 1; i < lines.length; i++) { const line = lines[i]; if (line.trim() === "") continue; @@ -90,7 +80,7 @@ export function useImportEft() { */ if (line.startsWith("[") || line.startsWith(" ")) { - if (lastSlotType != "") { + if (lastSlotType !== undefined && lastSlotType !== "DroneBay") { slotIndex[lastSlotType]++; } continue; @@ -110,7 +100,7 @@ export function useImportEft() { const attributes = eveData.typeDogma[itemTypeId]?.dogmaAttributes; /* Find what type of slot this item goes into. */ - let slotType = undefined; + let slotType: EsfSlotType | "DroneBay" | undefined = undefined; if (slotType === undefined && effects !== undefined) { for (const effectId in effects) { slotType = effectIdMapping[effects[effectId].effectID]; @@ -128,18 +118,31 @@ export function useImportEft() { if (slotType === undefined) continue; lastSlotType = slotType; - const flag = slotType === "droneBay" ? 87 : esiFlagMapping[slotType][slotIndex[slotType]]; let charge = undefined; if (chargeTypeId !== undefined) { charge = { - type_id: chargeTypeId, + typeId: chargeTypeId, }; } - fit.items.push({ - flag, - quantity: itemCount, - type_id: itemTypeId, + if (slotType === "DroneBay") { + fit.drones.push({ + typeId: itemTypeId, + states: { + Active: itemCount, + Passive: 0, + }, + }); + continue; + } + + fit.modules.push({ + slot: { + type: slotType, + index: slotIndex[slotType], + }, + typeId: itemTypeId, + state: "Active", charge, }); slotIndex[slotType]++; diff --git a/src/hooks/ImportEsiFitting/ImportEsiFitting.stories.tsx b/src/hooks/ImportEsiFitting/ImportEsiFitting.stories.tsx new file mode 100644 index 0000000..ca290bb --- /dev/null +++ b/src/hooks/ImportEsiFitting/ImportEsiFitting.stories.tsx @@ -0,0 +1,34 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; + +import { esiFits } from "../../../.storybook/fits"; + +import { EveDataProvider } from "@/providers/EveDataProvider"; + +import { ImportEsiFitting } from "./ImportEsiFitting"; + +const meta: Meta = { + component: ImportEsiFitting, + tags: ["autodocs"], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + argTypes: { + esiFit: { + control: "select", + options: Object.keys(esiFits), + mapping: esiFits, + }, + }, + decorators: [ + (Story) => ( + + + + ), + ], + render: (args) => , +}; diff --git a/src/hooks/ImportEsiFitting/ImportEsiFitting.tsx b/src/hooks/ImportEsiFitting/ImportEsiFitting.tsx new file mode 100644 index 0000000..8f68e44 --- /dev/null +++ b/src/hooks/ImportEsiFitting/ImportEsiFitting.tsx @@ -0,0 +1,98 @@ +import React from "react"; + +import { EsfCargo, EsfDrone, EsfFit, EsfModule } from "@/providers/CurrentFitProvider"; +import { useEveData } from "@/providers/EveDataProvider"; +import { esiFlagToEsfSlot } from "../ImportEveShipFitHash"; + +export interface EsiFit { + name: string; + description: string; + ship_type_id: number; + items: { + item_id: number; + type_id: number; + flag: number; + quantity: number; + }[]; +} + +/** + * Convert an ESI Fitting JSON to an ESF Fit. + */ +export function useImportEsiFitting() { + const eveData = useEveData(); + + return (esiFit: EsiFit): EsfFit | null => { + if (eveData === null) return null; + + const modules = esiFit.items + .map((item): EsfModule | undefined => { + const slot = esiFlagToEsfSlot[item.flag]; + if (slot === undefined) return undefined; + + return { + typeId: item.type_id, + slot, + state: "Active", + }; + }) + .filter((item): item is EsfModule => item !== undefined); + + const drones = esiFit.items + .map((item): EsfDrone | undefined => { + if (item.flag !== 87) return undefined; + + return { + typeId: item.type_id, + states: { + Active: item.quantity, + Passive: 0, + }, + }; + }) + .filter((item): item is EsfDrone => item !== undefined); + + const cargo = esiFit.items + .map((item): EsfCargo | undefined => { + if (item.flag !== 5) return undefined; + + return { + typeId: item.type_id, + quantity: item.quantity, + }; + }) + .filter((item): item is EsfCargo => item !== undefined); + + const fit: EsfFit = { + name: esiFit.name, + description: esiFit.description, + shipTypeId: esiFit.ship_type_id, + modules, + drones, + cargo, + }; + + return fit; + }; +} + +export interface FormatEftToEsiProps { + /** The ESI Fitting JSON. */ + esiFit: EsiFit; +} + +/** + * `useImportEsiFitting` converts an ESI Fitting JSON to an ESF fit. + * + * Note: do not use this React component itself, but the `useImportEsiFitting` React hook instead. + */ +export const ImportEsiFitting = (props: FormatEftToEsiProps) => { + const importEsiFitting = useImportEsiFitting(); + + if (props.esiFit === undefined) { + return
No fit selected.
; + } + + const fit = importEsiFitting(props.esiFit); + return
{JSON.stringify(fit, null, 2)}
; +}; diff --git a/src/hooks/ImportEsiFitting/index.ts b/src/hooks/ImportEsiFitting/index.ts new file mode 100644 index 0000000..0dd4737 --- /dev/null +++ b/src/hooks/ImportEsiFitting/index.ts @@ -0,0 +1,2 @@ +export { useImportEsiFitting } from "./ImportEsiFitting"; +export type { EsiFit } from "./ImportEsiFitting"; diff --git a/src/hooks/ImportEveShipFitHash/DecodeEft.tsx b/src/hooks/ImportEveShipFitHash/DecodeEft.tsx new file mode 100644 index 0000000..51a6e4b --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/DecodeEft.tsx @@ -0,0 +1,13 @@ +import { useImportEft } from "@/hooks/ImportEft"; +import { EsfFit } from "@/providers/CurrentFitProvider"; + +import { decompress } from "./Decompress"; + +export function useDecodeEft() { + const importEft = useImportEft(); + + return async (eftCompressed: string): Promise => { + const eft = await decompress(eftCompressed); + return importEft(eft); + }; +} diff --git a/src/hooks/ImportEveShipFitHash/DecodeEsfFitV1.tsx b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV1.tsx new file mode 100644 index 0000000..b008aa0 --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV1.tsx @@ -0,0 +1,29 @@ +import { EsfFit, EsfModule } from "@/providers/CurrentFitProvider"; + +import { decompress } from "./Decompress"; +import { esiFlagToEsfSlot } from "./EsiFlags"; + +export async function decodeEsfFitV1(fitCompressed: string): Promise { + const fitEncoded = await decompress(fitCompressed); + + const fitLines = fitEncoded.trim().split("\n"); + const fitHeader = fitLines[0].split(","); + + const modules = fitLines.slice(1).map((line): EsfModule => { + const item = line.split(","); + return { + slot: esiFlagToEsfSlot[parseInt(item[0])], + typeId: parseInt(item[1]), + state: "Active", + }; + }); + + return { + shipTypeId: parseInt(fitHeader[0]), + name: fitHeader[1], + description: fitHeader[2], + modules, + drones: [], // v1 didn't store drones. + cargo: [], // v2 didn't store cargo. + }; +} diff --git a/src/hooks/ImportEveShipFitHash/DecodeEsfFitV2.tsx b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV2.tsx new file mode 100644 index 0000000..f7ddb6b --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV2.tsx @@ -0,0 +1,86 @@ +import { EsfCargo, EsfDrone, EsfFit, EsfModule, EsfState } from "@/providers/CurrentFitProvider"; + +import { decompress } from "./Decompress"; +import { esiFlagToEsfSlot } from "./EsiFlags"; + +export async function decodeEsfFitV2(fitCompressed: string): Promise { + const fitEncoded = await decompress(fitCompressed); + + const fitLines = fitEncoded.trim().split("\n"); + const fitHeader = fitLines[0].split(","); + + const modules = fitLines + .slice(1) + .map((line): EsfModule | undefined => { + const item = line.split(","); + const flag = parseInt(item[0]); + if (esiFlagToEsfSlot[flag] === undefined) return undefined; // Skip anything not modules. + + let charge = undefined; + if (item[3]) { + charge = { + typeId: parseInt(item[3]), + }; + } + + return { + slot: esiFlagToEsfSlot[flag], + typeId: parseInt(item[1]), + charge, + state: (item[4] as EsfState) || "Active", + }; + }) + .filter((item): item is EsfModule => item !== undefined); + + const drones = fitLines + .slice(1) + .map((line): EsfDrone | undefined => { + const item = line.split(","); + const flag = parseInt(item[0]); + if (flag != 87) return undefined; // Skip anything not drones. + + const quantity = parseInt(item[2]); + + return { + typeId: parseInt(item[1]), + states: { + Active: item[4] !== "Passive" ? quantity : 0, + Passive: item[4] === "Passive" ? quantity : 0, + }, + }; + }) + .filter((item): item is EsfDrone => item !== undefined); + /* Drones can now be in the list twice, once for active and once for passive. Deduplicate. */ + const droneMap = new Map(); + drones.forEach((drone) => { + if (droneMap.has(drone.typeId)) { + droneMap.get(drone.typeId)!.states.Active += drone.states.Active; + droneMap.get(drone.typeId)!.states.Passive += drone.states.Passive; + } else { + droneMap.set(drone.typeId, drone); + } + }); + + const cargo = fitLines + .slice(1) + .map((line): EsfCargo | undefined => { + const item = line.split(","); + const flag = parseInt(item[0]); + if (flag != 5) return undefined; // Skip anything not cargo. + + return { + typeId: parseInt(item[1]), + quantity: parseInt(item[2]), + }; + }) + .filter((item): item is EsfCargo => item !== undefined); + + return { + shipTypeId: parseInt(fitHeader[0]), + name: fitHeader[1], + description: fitHeader[2], + modules, + drones: Array.from(droneMap.values()), + cargo, + }; +} diff --git a/src/hooks/ImportEveShipFitHash/DecodeEsfFitV3.tsx b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV3.tsx new file mode 100644 index 0000000..2a98ed1 --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/DecodeEsfFitV3.tsx @@ -0,0 +1,67 @@ +import { EsfCargo, EsfDrone, EsfFit, EsfModule, EsfSlotType, EsfState } from "@/providers/CurrentFitProvider"; + +import { decompress } from "./Decompress"; + +export async function decodeEsfFitV3(fitCompressed: string): Promise { + const fitEncoded = await decompress(fitCompressed); + + const fitLines = fitEncoded.trim().split("\n"); + const fitHeader = fitLines[0].split(","); + + const modules = fitLines + .slice(1) + .map((line): EsfModule | undefined => { + const item = line.split(","); + const type = item[0]; + if (type !== "module") return undefined; + + return { + slot: { + type: item[1] as EsfSlotType, + index: parseInt(item[2]), + }, + typeId: parseInt(item[3]), + state: item[4] as EsfState, + charge: item[5] ? { typeId: parseInt(item[5]) } : undefined, + }; + }) + .filter((item): item is EsfModule => item !== undefined); + const drones = fitLines + .slice(1) + .map((line): EsfDrone | undefined => { + const item = line.split(","); + const type = item[0]; + if (type !== "drone") return undefined; + + return { + typeId: parseInt(item[1]), + states: { + Active: parseInt(item[2]), + Passive: parseInt(item[3]), + }, + }; + }) + .filter((item): item is EsfDrone => item !== undefined); + const cargo = fitLines + .slice(1) + .map((line): EsfCargo | undefined => { + const item = line.split(","); + const type = item[0]; + if (type !== "cargo") return undefined; + + return { + typeId: parseInt(item[1]), + quantity: parseInt(item[2]), + }; + }) + .filter((item): item is EsfCargo => item !== undefined); + + return { + shipTypeId: parseInt(fitHeader[1]), + name: fitHeader[2], + description: fitHeader[3], + modules, + drones, + cargo, + }; +} diff --git a/src/hooks/ImportEveShipFitHash/DecodeKillMail.tsx b/src/hooks/ImportEveShipFitHash/DecodeKillMail.tsx new file mode 100644 index 0000000..192324c --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/DecodeKillMail.tsx @@ -0,0 +1,110 @@ +import { EsfCargo, EsfDrone, EsfFit, EsfModule } from "@/providers/CurrentFitProvider"; +import { useEveData } from "@/providers/EveDataProvider"; + +import { esiFlagToEsfSlot } from "./EsiFlags"; + +export function useFetchKillMail() { + const eveData = useEveData(); + + return async (killMailHash: string): Promise => { + if (eveData === null) return null; + + /* The hash is in the format "id/hash". */ + const [killmailId, killmailHash] = killMailHash.split("/", 2); + + /* Fetch the killmail from ESI. */ + const response = await fetch(`https://esi.evetech.net/v1/killmails/${killmailId}/${killmailHash}/`); + if (response.status !== 200) return null; + + const killMail = await response.json(); + + /* Convert the killmail items to a flatter list (dropped vs destroyed). */ + type KillMailItem = { + flag: number; + type_id: number; + quantity: number; + }; + const items: KillMailItem[] = killMail.victim.items.map( + (item: { flag: number; item_type_id: number; quantity_destroyed?: number; quantity_dropped?: number }) => { + return { + flag: item.flag, + type_id: item.item_type_id, + quantity: (item.quantity_dropped ?? 0) + (item.quantity_destroyed ?? 0), + }; + }, + ); + + /* Find the modules from the item-list. */ + let modules = items + .map((item): EsfModule | undefined => { + if (esiFlagToEsfSlot[item.flag] === undefined) return undefined; // Skip anything not modules. + + return { + slot: esiFlagToEsfSlot[item.flag], + typeId: item.type_id, + charge: undefined, + state: "Active", + }; + }) + .filter((item): item is EsfModule => item !== undefined); + + /* Find the drones from the item-list. */ + const drones = items + .map((item): EsfDrone | undefined => { + if (item.flag !== 87) return undefined; // Skip anything not drones. + + return { + typeId: item.type_id, + states: { + Active: item.quantity, + Passive: 0, + }, + }; + }) + .filter((item): item is EsfDrone => item !== undefined); + + /* Find the cargo from the item-list. */ + const cargo = items + .map((item): EsfCargo | undefined => { + if (item.flag !== 5) return undefined; // Skip anything not cargo. + + return { + typeId: item.type_id, + quantity: item.quantity, + }; + }) + .filter((item): item is EsfCargo => item !== undefined); + + /* When importing fits, it can be that the ammo is on the same slot as the module, instead as charge. Fix that. */ + modules = modules + .map((moduleOrCharge) => { + /* Looks for items that are charges. */ + if (eveData.typeIDs[moduleOrCharge.typeId]?.categoryID !== 8) return moduleOrCharge; + + /* Find the module on the same slot. */ + const module = modules.find( + (itemModule) => itemModule.slot === moduleOrCharge.slot && itemModule.typeId !== moduleOrCharge.typeId, + ); + + if (module !== undefined) { + /* Assign the charge to the module. */ + module.charge = { + typeId: moduleOrCharge.typeId, + }; + } + + /* Remove the charge from the slot. */ + return undefined; + }) + .filter((item): item is EsfModule => item !== undefined); + + return { + shipTypeId: killMail.victim.ship_type_id, + name: `Killmail ${killmailId}`, + description: "", + modules, + drones, + cargo, + }; + }; +} diff --git a/src/hooks/ImportEveShipFitHash/Decompress.tsx b/src/hooks/ImportEveShipFitHash/Decompress.tsx new file mode 100644 index 0000000..34b9d67 --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/Decompress.tsx @@ -0,0 +1,15 @@ +export async function decompress(base64compressedBytes: string): Promise { + const stream = new Blob([Uint8Array.from(atob(base64compressedBytes), (c) => c.charCodeAt(0))]).stream(); + const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip")); + const reader = decompressedStream.getReader(); + + let result = ""; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + result += String.fromCharCode.apply(null, value); + } + + return result; +} diff --git a/src/hooks/ImportEveShipFitHash/EsiFlags.tsx b/src/hooks/ImportEveShipFitHash/EsiFlags.tsx new file mode 100644 index 0000000..604e02e --- /dev/null +++ b/src/hooks/ImportEveShipFitHash/EsiFlags.tsx @@ -0,0 +1,35 @@ +import { EsfSlot } from "@/providers/CurrentFitProvider"; + +export const esiFlagToEsfSlot: Record = { + 11: { type: "Low", index: 1 }, + 12: { type: "Low", index: 2 }, + 13: { type: "Low", index: 3 }, + 14: { type: "Low", index: 4 }, + 15: { type: "Low", index: 5 }, + 16: { type: "Low", index: 6 }, + 17: { type: "Low", index: 7 }, + 18: { type: "Low", index: 8 }, + 19: { type: "Medium", index: 1 }, + 20: { type: "Medium", index: 2 }, + 21: { type: "Medium", index: 3 }, + 22: { type: "Medium", index: 4 }, + 23: { type: "Medium", index: 5 }, + 24: { type: "Medium", index: 6 }, + 25: { type: "Medium", index: 7 }, + 26: { type: "Medium", index: 8 }, + 27: { type: "High", index: 1 }, + 28: { type: "High", index: 2 }, + 29: { type: "High", index: 3 }, + 30: { type: "High", index: 4 }, + 31: { type: "High", index: 5 }, + 32: { type: "High", index: 6 }, + 33: { type: "High", index: 7 }, + 34: { type: "High", index: 8 }, + 92: { type: "Rig", index: 1 }, + 93: { type: "Rig", index: 2 }, + 94: { type: "Rig", index: 3 }, + 125: { type: "SubSystem", index: 1 }, + 126: { type: "SubSystem", index: 2 }, + 127: { type: "SubSystem", index: 3 }, + 128: { type: "SubSystem", index: 4 }, +}; diff --git a/src/hooks/ImportEveShipFitHash/ImportEveShipFitHash.tsx b/src/hooks/ImportEveShipFitHash/ImportEveShipFitHash.tsx index b2b9fa3..ad38cbe 100644 --- a/src/hooks/ImportEveShipFitHash/ImportEveShipFitHash.tsx +++ b/src/hooks/ImportEveShipFitHash/ImportEveShipFitHash.tsx @@ -1,150 +1,12 @@ import React from "react"; import { EsfFit } from "@/providers/CurrentFitProvider"; -import { useEveData } from "@/providers/EveDataProvider"; -import { useImportEft } from "../ImportEft"; -async function decompress(base64compressedBytes: string): Promise { - const stream = new Blob([Uint8Array.from(atob(base64compressedBytes), (c) => c.charCodeAt(0))]).stream(); - const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip")); - const reader = decompressedStream.getReader(); - - let result = ""; - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - result += String.fromCharCode.apply(null, value); - } - - return result; -} - -async function decodeEsfFitV1(fitCompressed: string): Promise { - const fitEncoded = await decompress(fitCompressed); - - const fitLines = fitEncoded.trim().split("\n"); - const fitHeader = fitLines[0].split(","); - - const fitItems = fitLines.slice(1).map((line) => { - const item = line.split(","); - return { - flag: parseInt(item[0]), - type_id: parseInt(item[1]), - quantity: parseInt(item[2]), - }; - }); - - return { - ship_type_id: parseInt(fitHeader[0]), - name: fitHeader[1], - description: fitHeader[2], - items: fitItems, - }; -} - -async function decodeEsfFitV2(fitCompressed: string): Promise { - const fitEncoded = await decompress(fitCompressed); - - const fitLines = fitEncoded.trim().split("\n"); - const fitHeader = fitLines[0].split(","); - - const fitItems = fitLines.slice(1).map((line) => { - const item = line.split(","); - - let charge = undefined; - if (item[3]) { - charge = { - type_id: parseInt(item[3]), - }; - } - - return { - flag: parseInt(item[0]), - type_id: parseInt(item[1]), - quantity: parseInt(item[2]), - charge, - state: item[4] || undefined, - }; - }); - - return { - ship_type_id: parseInt(fitHeader[0]), - name: fitHeader[1], - description: fitHeader[2], - items: fitItems, - }; -} - -function useFetchKillMail() { - const eveData = useEveData(); - - return async (killMailHash: string): Promise => { - if (eveData === null) return null; - - /* The hash is in the format "id/hash". */ - const [killmailId, killmailHash] = killMailHash.split("/", 2); - - /* Fetch the killmail from ESI. */ - const response = await fetch(`https://esi.evetech.net/v1/killmails/${killmailId}/${killmailHash}/`); - if (response.status !== 200) return null; - - const killMail = await response.json(); - - /* Convert the killmail to a fit; be mindful that ammo and a module can be on the same slot. */ - let fitItems: EsfFit["items"] = killMail.victim.items.map( - (item: { flag: number; item_type_id: number; quantity_destroyed?: number; quantity_dropped?: number }) => { - return { - flag: item.flag, - type_id: item.item_type_id, - quantity: (item.quantity_dropped ?? 0) + (item.quantity_destroyed ?? 0), - }; - }, - ); - - fitItems = fitItems - .map((item) => { - /* When importing fits, it can be that the ammo is on the same slot as the module, instead as charge. Fix that. */ - - /* Ignore cargobay. */ - if (item.flag === 5) return item; - /* Looks for items that are charges. */ - if (eveData.typeIDs[item.type_id]?.categoryID !== 8) return item; - - /* Find the module on the same slot. */ - const module = fitItems.find( - (itemModule) => itemModule.flag === item.flag && itemModule.type_id !== item.type_id, - ); - - if (module !== undefined) { - /* Assign the charge to the module. */ - module.charge = { - type_id: item.type_id, - }; - } - - /* Remove the charge from the slot. */ - return undefined; - }) - .filter((item): item is EsfFit["items"][number] => item !== undefined); - - return { - ship_type_id: killMail.victim.ship_type_id, - name: `Killmail ${killmailId}`, - description: "", - items: fitItems, - }; - }; -} - -function useDecodeEft() { - const importEft = useImportEft(); - - return async (eftCompressed: string): Promise => { - const eft = await decompress(eftCompressed); - return importEft(eft); - }; -} +import { decodeEsfFitV1 } from "./DecodeEsfFitV1"; +import { decodeEsfFitV2 } from "./DecodeEsfFitV2"; +import { decodeEsfFitV3 } from "./DecodeEsfFitV3"; +import { useDecodeEft } from "./DecodeEft"; +import { useFetchKillMail } from "./DecodeKillMail"; /** * Convert a hash from window.location.hash to an ESI fit. @@ -168,6 +30,9 @@ export function useImportEveShipFitHash() { case "v2": fit = await decodeEsfFitV2(fitEncoded); break; + case "v3": + fit = await decodeEsfFitV3(fitEncoded); + break; case "killmail": fit = await fetchKillMail(fitEncoded); break; @@ -193,13 +58,20 @@ export const ImportEveShipFitHash = (props: ImportEveShipFitHashProps) => { const importEveShipFitHash = useImportEveShipFitHash(); const [fit, setFit] = React.useState(undefined); + const importEveShipFitHashRef = React.useRef(importEveShipFitHash); + importEveShipFitHashRef.current = importEveShipFitHash; + React.useEffect(() => { async function getFit(fitHash: string) { - setFit(await importEveShipFitHash(fitHash)); + setFit(await importEveShipFitHashRef.current(fitHash)); } getFit(props.fitHash); - }, [props.fitHash, importEveShipFitHash]); + }, [props.fitHash]); + + if (props.fitHash === undefined) { + return
Select a fit hash.
; + } return (
diff --git a/src/hooks/ImportEveShipFitHash/index.ts b/src/hooks/ImportEveShipFitHash/index.ts index c58ec7f..c508fff 100644 --- a/src/hooks/ImportEveShipFitHash/index.ts +++ b/src/hooks/ImportEveShipFitHash/index.ts @@ -1 +1,2 @@ export { useImportEveShipFitHash } from "./ImportEveShipFitHash"; +export { esiFlagToEsfSlot } from "./EsiFlags"; diff --git a/src/index.ts b/src/index.ts index 48c55d1..47d8614 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ export * from "./hooks/Clipboard"; export * from "./hooks/ExportEft"; export * from "./hooks/ExportEveShipFitHash"; export * from "./hooks/ImportEft"; +export * from "./hooks/ImportEsiFitting"; export * from "./hooks/ImportEveShipFitHash"; export * from "./hooks/LocalStorage"; export * from "./providers/Characters"; diff --git a/src/providers/Characters/EsiCharactersProvider/EsiCharactersProvider.tsx b/src/providers/Characters/EsiCharactersProvider/EsiCharactersProvider.tsx index f58323c..e0b4a5f 100644 --- a/src/providers/Characters/EsiCharactersProvider/EsiCharactersProvider.tsx +++ b/src/providers/Characters/EsiCharactersProvider/EsiCharactersProvider.tsx @@ -1,7 +1,9 @@ import React from "react"; import { Character } from "@/providers/CurrentCharacterProvider"; +import { EsfFit } from "@/providers/CurrentFitProvider"; import { useEveData } from "@/providers/EveDataProvider"; +import { useImportEsiFitting } from "@/hooks/ImportEsiFitting"; import { useLocalStorage } from "@/hooks/LocalStorage"; import { CharactersContext, useCharactersInternal } from "../CharactersContext"; @@ -61,6 +63,7 @@ const createEmptyCharacter = (name: string): Character => { export const EsiCharactersProvider = (props: EsiProps) => { const characters = useCharactersInternal(); const eveData = useEveData(); + const importEsiFitting = useImportEsiFitting(); const [firstLoad, setFirstLoad] = React.useState(true); @@ -134,8 +137,8 @@ export const EsiCharactersProvider = (props: EsiProps) => { const skills = await getSkills(characterId, accessToken); if (skills === undefined) return; - const fittings = await getCharFittings(characterId, accessToken); - if (fittings === undefined) return; + const esiFittings = await getCharFittings(characterId, accessToken); + if (esiFittings === undefined) return; /* Ensure all skills are set; also those not learnt. */ for (const typeId in eveData.typeIDs) { @@ -144,6 +147,13 @@ export const EsiCharactersProvider = (props: EsiProps) => { skills[typeId] = 0; } + /* Convert all fittings to ESF format. */ + const fittings = esiFittings + .map((fitting) => { + return importEsiFitting(fitting); + }) + .filter((fitting): fitting is EsfFit => fitting !== null); + setEsiCharacters((oldEsiCharacters: Record) => { return { ...oldEsiCharacters, @@ -155,7 +165,7 @@ export const EsiCharactersProvider = (props: EsiProps) => { }; }); }, - [setEsiCharacters, ensureAccessToken, eveData], + [setEsiCharacters, importEsiFitting, ensureAccessToken, eveData], ); if (firstLoad) { diff --git a/src/providers/Characters/EsiCharactersProvider/EsiGetFittings.tsx b/src/providers/Characters/EsiCharactersProvider/EsiGetFittings.tsx index 6e08831..e3c7dd3 100644 --- a/src/providers/Characters/EsiCharactersProvider/EsiGetFittings.tsx +++ b/src/providers/Characters/EsiCharactersProvider/EsiGetFittings.tsx @@ -1,6 +1,6 @@ -import { EsfFit } from "@/providers/CurrentFitProvider"; +import { EsiFit } from "@/hooks/ImportEsiFitting"; -export async function getCharFittings(characterId: string, accessToken: string): Promise { +export async function getCharFittings(characterId: string, accessToken: string): Promise { let response; try { response = await fetch(`https://esi.evetech.net/v1/characters/${characterId}/fittings/`, { diff --git a/src/providers/CurrentFitProvider/CurrentFitProvider.tsx b/src/providers/CurrentFitProvider/CurrentFitProvider.tsx index 9cf8edb..961b3c7 100644 --- a/src/providers/CurrentFitProvider/CurrentFitProvider.tsx +++ b/src/providers/CurrentFitProvider/CurrentFitProvider.tsx @@ -1,21 +1,44 @@ import React from "react"; -export type State = "Passive" | "Online" | "Active" | "Overload"; +export type EsfState = "Passive" | "Online" | "Active" | "Overload"; +export type EsfSlotType = "High" | "Medium" | "Low" | "Rig" | "SubSystem"; + +export interface EsfCharge { + typeId: number; +} + +export interface EsfSlot { + type: EsfSlotType; + index: number; +} + +export interface EsfModule { + typeId: number; + slot: EsfSlot; + state: EsfState; + charge?: EsfCharge; +} + +export interface EsfDrone { + typeId: number; + states: { + Passive: number; + Active: number; + }; +} + +export interface EsfCargo { + typeId: number; + quantity: number; +} export interface EsfFit { name: string; description: string; - ship_type_id: number; - items: { - type_id: number; - quantity: number; - flag: number; - charge?: { - type_id: number; - }; - /* State defaults to "Active" if not set. */ - state?: State | string; - }[]; + shipTypeId: number; + modules: EsfModule[]; + drones: EsfDrone[]; + cargo: EsfCargo[]; } interface CurrentFit { diff --git a/src/providers/CurrentFitProvider/index.ts b/src/providers/CurrentFitProvider/index.ts index 10e8d34..d4e4c4a 100644 --- a/src/providers/CurrentFitProvider/index.ts +++ b/src/providers/CurrentFitProvider/index.ts @@ -1,2 +1,11 @@ export { useCurrentFit, CurrentFitProvider } from "./CurrentFitProvider"; -export type { EsfFit, State } from "./CurrentFitProvider"; +export type { + EsfCargo, + EsfCharge, + EsfDrone, + EsfFit, + EsfModule, + EsfSlot, + EsfSlotType, + EsfState, +} from "./CurrentFitProvider"; diff --git a/src/providers/DogmaEngineProvider/DataTypes.tsx b/src/providers/DogmaEngineProvider/DataTypes.tsx new file mode 100644 index 0000000..02e0ff2 --- /dev/null +++ b/src/providers/DogmaEngineProvider/DataTypes.tsx @@ -0,0 +1,41 @@ +export interface CalculationItemAttributeEffect { + operator: string; + penalty: boolean; + source: "Ship" | "Char" | "Structure" | "Target" | { Item?: number; Charge?: number; Skill?: number }; + source_category: string; + source_attribute_id: number; +} + +export interface CalculationItemAttribute { + base_value: number; + value: number; + effects: CalculationItemAttributeEffect[]; +} + +export type CalculationSlotType = "High" | "Medium" | "Low" | "Rig" | "SubSystem" | "DroneBay" | "Charge"; + +export interface CalculationSlot { + type: CalculationSlotType; + index: number | undefined; +} + +export type CalculationState = "Passive" | "Online" | "Active" | "Overload" | "Target" | "Area" | "Dungeon" | "System"; + +export interface CalculationItem { + type_id: number; + slot: CalculationSlot; + charge: CalculationItem | undefined; + state: CalculationState; + max_state: CalculationState; + attributes: Map; + effects: number[]; +} + +export interface Calculation { + hull: CalculationItem; + items: CalculationItem[]; + skills: CalculationItem[]; + char: CalculationItem; + structure: CalculationItem; + target: CalculationItem; +} diff --git a/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx b/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx index a8eaa60..d8349e7 100644 --- a/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx +++ b/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx @@ -1,7 +1,5 @@ import React from "react"; -import type { init, calculate } from "@eveshipfit/dogma-engine"; - import { DogmaAttribute, DogmaEffect, @@ -10,10 +8,14 @@ import { TypeID, useEveData, } from "@/providers/EveDataProvider"; +import { EsfFit } from "@/providers/CurrentFitProvider"; +import { Skills } from "@/providers/CurrentCharacterProvider"; +import { calculate } from "@eveshipfit/dogma-engine"; + +import { Calculation } from "./DataTypes"; interface DogmaEngine { - init: typeof init; - calculate: typeof calculate; + calculate: (fit: EsfFit, skills: Skills) => Calculation; } const DogmaEngineContext = React.createContext(null); @@ -60,7 +62,9 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => { const [firstLoad, setFirstLoad] = React.useState(true); - const [dogmaEngine, setDogmaEngine] = React.useState(null); + const [dogmaEngine, setDogmaEngine] = React.useState<{ + calculate: typeof calculate; + } | null>(null); if (firstLoad) { setFirstLoad(false); @@ -90,7 +94,44 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => { } const contextValue = React.useMemo(() => { - return eveData === null ? null : dogmaEngine; + if (eveData === null || dogmaEngine === null) return null; + + return { + calculate: (fit: EsfFit, skills: Skills): Calculation => { + const dogmaFit = { + ship_type_id: fit.shipTypeId, + modules: fit.modules.map((module) => ({ + type_id: module.typeId, + slot: module.slot, + state: module.state, + charge: + module.charge === undefined + ? undefined + : { + type_id: module.charge.typeId, + }, + })), + drones: fit.drones.flatMap((drone) => { + const drones = []; + for (let i = 0; i < drone.states.Active; i++) { + drones.push({ + type_id: drone.typeId, + state: "Active", + }); + } + for (let i = 0; i < drone.states.Passive; i++) { + drones.push({ + type_id: drone.typeId, + state: "Passive", + }); + } + return drones; + }), + }; + + return dogmaEngine.calculate(dogmaFit, skills); + }, + }; }, [eveData, dogmaEngine]); return {props.children}; diff --git a/src/providers/DogmaEngineProvider/index.ts b/src/providers/DogmaEngineProvider/index.ts index 1014b1c..873eef1 100644 --- a/src/providers/DogmaEngineProvider/index.ts +++ b/src/providers/DogmaEngineProvider/index.ts @@ -1 +1,10 @@ export { DogmaEngineProvider, useDogmaEngine } from "./DogmaEngineProvider"; +export type { + Calculation, + CalculationItem, + CalculationItemAttribute, + CalculationItemAttributeEffect, + CalculationSlot, + CalculationSlotType, + CalculationState, +} from "./DataTypes"; diff --git a/src/providers/FitManagerProvider/FitManagerProvider.tsx b/src/providers/FitManagerProvider/FitManagerProvider.tsx index 4c3d6b5..265244e 100644 --- a/src/providers/FitManagerProvider/FitManagerProvider.tsx +++ b/src/providers/FitManagerProvider/FitManagerProvider.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { EsfFit, State, useCurrentFit } from "@/providers/CurrentFitProvider"; -import { StatisticsSlotType, useStatistics } from "@/providers/StatisticsProvider"; +import { EsfFit, EsfSlot, EsfSlotType, EsfState, useCurrentFit } from "@/providers/CurrentFitProvider"; +import { useStatistics } from "@/providers/StatisticsProvider"; import { useEveData } from "@/providers/EveDataProvider"; interface FitManager { @@ -13,21 +13,21 @@ interface FitManager { setName: (name: string) => void; /** Add an item (module, charge, drone) to the fit. */ - addItem: (typeId: number, slot: StatisticsSlotType | "droneBay" | "charge") => void; + addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => void; /** Set a module in a slot. */ - setModule: (flag: number, typeId: number) => void; + setModule: (slot: EsfSlot, typeId: number) => void; /** Set the state of a module. */ - setModuleState: (flag: number, state: State) => void; + setModuleState: (slot: EsfSlot, state: EsfState) => void; /** Remove a module from a slot. */ - removeModule: (flag: number) => void; + removeModule: (slot: EsfSlot) => void; /** Swap two modules. */ - swapModule: (flagA: number, flagB: number) => void; + swapModule: (slotA: EsfSlot, slotB: EsfSlot) => void; /** Set a charge in a module. */ - setCharge: (flag: number, typeId: number) => void; + setCharge: (slot: EsfSlot, typeId: number) => void; /** Remove a charge from a module. */ - removeCharge: (flag: number) => void; + removeCharge: (slot: EsfSlot) => void; /** Activate N drones of a given type. */ activateDrones: (typeId: number, amount: number) => void; @@ -35,16 +35,6 @@ interface FitManager { removeDrones: (typeId: number) => void; } -const slotStart: Record = { - hislot: 27, - medslot: 19, - lowslot: 11, - subsystem: 125, - rig: 92, - launcher: 0, - turret: 0, -}; - const FitManagerContext = React.createContext({ setFit: () => {}, createNewFit: () => {}, @@ -112,12 +102,14 @@ export const FitManagerProvider = (props: FitManagerProps) => { setFit({ name: "Unnamed Fit", description: "", - ship_type_id: typeId, - items: [], + shipTypeId: typeId, + modules: [], + drones: [], + cargo: [], }); }, setName: (name: string) => { - setFit((oldFit) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; return { @@ -127,30 +119,30 @@ export const FitManagerProvider = (props: FitManagerProps) => { }); }, - addItem: (typeId: number, slot: StatisticsSlotType | "droneBay" | "charge") => { - setFit((oldFit) => { + addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; - if (slot === "charge") { + if (slot === "Charge") { const chargeSize = eveData.typeDogma[typeId]?.dogmaAttributes.find( (attr) => attr.attributeID === eveData.attributeMapping?.chargeSize, )?.value ?? -1; const groupID = eveData.typeIDs[typeId]?.groupID ?? -1; - const newItems = []; - for (let item of oldFit.items) { + const newModules = []; + for (let module of oldFit.modules) { /* If the module has size restrictions, ensure the charge matches. */ - const moduleChargeSize = eveData.typeDogma[item.type_id]?.dogmaAttributes.find( + const moduleChargeSize = eveData.typeDogma[module.typeId]?.dogmaAttributes.find( (attr) => attr.attributeID === eveData.attributeMapping.chargeSize, )?.value; if (moduleChargeSize !== undefined && moduleChargeSize !== chargeSize) { - newItems.push(item); + newModules.push(module); continue; } /* Check if the charge fits in this module; if so, assign it. */ - for (const attr of eveData.typeDogma[item.type_id]?.dogmaAttributes ?? []) { + for (const attr of eveData.typeDogma[module.typeId]?.dogmaAttributes ?? []) { switch (attr.attributeID) { case eveData.attributeMapping.chargeGroup1: case eveData.attributeMapping.chargeGroup2: @@ -158,10 +150,10 @@ export const FitManagerProvider = (props: FitManagerProps) => { case eveData.attributeMapping.chargeGroup4: case eveData.attributeMapping.chargeGroup5: if (attr.value === groupID) { - item = { - ...item, + module = { + ...module, charge: { - type_id: typeId, + typeId, }, }; } @@ -169,73 +161,92 @@ export const FitManagerProvider = (props: FitManagerProps) => { } } - newItems.push(item); + newModules.push(module); } return { ...oldFit, - items: newItems, + modules: newModules, }; } - let flag = undefined; + if (slot === "DroneBay") { + const drone = oldFit.drones.find((item) => item.typeId === typeId); + + if (drone !== undefined) { + drone.states.Active++; + return oldFit; + } + + return { + ...oldFit, + drones: [ + ...oldFit.drones, + { + typeId: typeId, + states: { + Active: 1, + Passive: 0, + }, + }, + ], + }; + } /* Find the first free slot for that slot-type. */ - if (slot !== "droneBay") { - const slotsAvailable = statistics?.slots[slot] ?? 0; - for (let i = slotStart[slot]; i < slotStart[slot] + slotsAvailable; i++) { - if (oldFit.items.find((item) => item.flag === i) !== undefined) continue; + let index = undefined; + const slotsAvailable = statistics?.slots[slot] ?? 0; + for (let i = 1; i <= slotsAvailable; i++) { + if (oldFit.modules.find((item) => item.slot.type === slot && item.slot.index === i) !== undefined) continue; - flag = i; - break; - } - console.log(flag); - } else { - flag = 87; + index = i; + break; } /* Couldn't find a free slot. */ - if (flag === undefined) return oldFit; + if (index === undefined) return oldFit; return { ...oldFit, - items: [ - ...oldFit.items, + modules: [ + ...oldFit.modules, { - flag: flag, - type_id: typeId, - quantity: 1, + slot: { + type: slot, + index: index, + }, + typeId: typeId, + charge: undefined, + state: "Active", }, ], }; }); }, - setModule: (flag: number, typeId: number) => { - setFit((oldFit) => { + setModule: (slot: EsfSlot, typeId: number) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; - const newItems = oldFit.items - .filter((item) => item.flag !== flag) - .concat({ flag: flag, type_id: typeId, quantity: 1 }); - return { ...oldFit, - items: newItems, + modules: oldFit.modules + .filter((item) => item.slot.type !== slot.type || item.slot.index !== slot.index) + .concat({ slot, typeId, state: "Active" }), }; }); }, - setModuleState: (flag: number, state: State) => { - setFit((oldFit) => { + setModuleState: (slot: EsfSlot, state: EsfState) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; return { ...oldFit, - items: oldFit?.items.map((item) => { - if (item.flag === flag) { + modules: oldFit.modules.map((item) => { + if (item.slot.type === slot.type && item.slot.index === slot.index) { return { ...item, - state: state, + state, }; } @@ -244,70 +255,68 @@ export const FitManagerProvider = (props: FitManagerProps) => { }; }); }, - removeModule: (flag: number) => { - setFit((oldFit) => { + removeModule: (slot: EsfSlot) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; return { ...oldFit, - items: oldFit.items.filter((item) => item.flag !== flag), + modules: oldFit.modules.filter((item) => item.slot.type !== slot.type || item.slot.index !== slot.index), }; }); }, - swapModule: (flagA: number, flagB: number) => { - setFit((oldFit) => { + swapModule: (slotA: EsfSlot, slotB: EsfSlot) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; - const newItems = [...oldFit.items]; - - const fromItemIndex = newItems.findIndex((item) => item.flag === flagA); - const fromItem = newItems[fromItemIndex]; + const modules = [...oldFit.modules]; - const toItemIndex = newItems.findIndex((item) => item.flag === flagB); - const toItem = newItems[toItemIndex]; + const moduleA = modules.find((item) => item.slot.type === slotA.type && item.slot.index === slotA.index); + const moduleB = modules.find((item) => item.slot.type === slotB.type && item.slot.index === slotB.index); - fromItem.flag = flagB; + if (moduleA !== undefined) { + moduleA.slot.index = slotB.index; + } - if (toItem !== undefined) { - /* Target slot is non-empty, swap items. */ - toItem.flag = flagA; + if (moduleB !== undefined) { + moduleB.slot.index = slotA.index; } return { ...oldFit, - items: newItems, + modules, }; }); }, - setCharge: (flag: number, typeId: number) => { + setCharge: (slot: EsfSlot, typeId: number) => { const chargeSize = eveData.typeDogma[typeId]?.dogmaAttributes.find( (attr) => attr.attributeID === eveData.attributeMapping?.chargeSize, )?.value ?? -1; const groupID = eveData.typeIDs[typeId]?.groupID ?? -1; - setFit((oldFit) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; - const newItems = []; + const modules = []; - for (let item of oldFit.items) { + for (let module of oldFit.modules) { /* If the module has size restrictions, ensure the charge matches. */ - const moduleChargeSize = eveData.typeDogma[item.type_id]?.dogmaAttributes.find( + const moduleChargeSize = eveData.typeDogma[module.typeId]?.dogmaAttributes.find( (attr) => attr.attributeID === eveData.attributeMapping.chargeSize, )?.value; if (moduleChargeSize !== undefined && moduleChargeSize !== chargeSize) { - newItems.push(item); + modules.push(module); continue; } - if (item.flag !== flag) { - newItems.push(item); + if (module.slot.type !== slot.type || module.slot.index !== slot.index) { + modules.push(module); continue; } /* Check if the charge fits in this module; if so, assign it. */ - for (const attr of eveData.typeDogma[item.type_id]?.dogmaAttributes ?? []) { + for (const attr of eveData.typeDogma[module.typeId]?.dogmaAttributes ?? []) { switch (attr.attributeID) { case eveData.attributeMapping.chargeGroup1: case eveData.attributeMapping.chargeGroup2: @@ -315,10 +324,10 @@ export const FitManagerProvider = (props: FitManagerProps) => { case eveData.attributeMapping.chargeGroup4: case eveData.attributeMapping.chargeGroup5: if (attr.value === groupID) { - item = { - ...item, + module = { + ...module, charge: { - type_id: typeId, + typeId: typeId, }, }; } @@ -326,23 +335,23 @@ export const FitManagerProvider = (props: FitManagerProps) => { } } - newItems.push(item); + modules.push(module); } return { ...oldFit, - items: newItems, + modules, }; }); }, - removeCharge: (flag: number) => { - setFit((oldFit) => { + removeCharge: (slot: EsfSlot) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; return { ...oldFit, - items: oldFit.items.map((item) => { - if (item.flag === flag) { + modules: oldFit.modules.map((item) => { + if (item.slot.type === slot.type && item.slot.index === slot.index) { return { ...item, charge: undefined, @@ -356,19 +365,19 @@ export const FitManagerProvider = (props: FitManagerProps) => { }, activateDrones: (typeId: number, active: number) => { - setFit((oldFit) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; /* Find the amount of drones in the current fit. */ - const count = oldFit.items - .filter((item) => item.flag === 87 && item.type_id === typeId) - .reduce((acc, item) => acc + item.quantity, 0); + const count = oldFit.drones + .filter((item) => item.typeId === typeId) + .reduce((acc, item) => acc + item.states.Active + item.states.Passive, 0); if (count === 0) return oldFit; /* If we request the same amount of active than we had, assume we want to deactivate the current. */ - const currentActive = oldFit.items - .filter((item) => item.flag === 87 && item.type_id === typeId && item.state === "Active") - .reduce((acc, item) => acc + item.quantity, 0); + const currentActive = oldFit.drones + .filter((item) => item.typeId === typeId) + .reduce((acc, item) => acc + item.states.Active, 0); if (currentActive === active) { active = active - 1; } @@ -377,41 +386,30 @@ export const FitManagerProvider = (props: FitManagerProps) => { active = Math.min(count, active); /* Remove all drones of this type. */ - const newItems = oldFit.items.filter((item) => item.flag !== 87 || item.type_id !== typeId); - - /* Add the active drones. */ - if (active > 0) { - newItems.push({ - flag: 87, - type_id: typeId, - quantity: active, - state: "Active", - }); - } + const drones = oldFit.drones.filter((item) => item.typeId !== typeId); + const passive = count - active; - /* Add the passive drones. */ - if (active < count) { - newItems.push({ - flag: 87, - type_id: typeId, - quantity: count - active, - state: "Passive", - }); - } + drones.push({ + typeId, + states: { + Active: active, + Passive: passive, + }, + }); return { ...oldFit, - items: newItems, + drones, }; }); }, removeDrones: (typeId: number) => { - setFit((oldFit) => { + setFit((oldFit: EsfFit | null): EsfFit | null => { if (oldFit === null) return null; return { ...oldFit, - items: oldFit.items.filter((item) => item.flag !== 87 || item.type_id !== typeId), + drones: oldFit.drones.filter((item) => item.typeId !== typeId), }; }); }, diff --git a/src/providers/LocalFitsProvider/ConvertV2.tsx b/src/providers/LocalFitsProvider/ConvertV2.tsx new file mode 100644 index 0000000..959d57d --- /dev/null +++ b/src/providers/LocalFitsProvider/ConvertV2.tsx @@ -0,0 +1,71 @@ +import { esiFlagToEsfSlot } from "@/hooks/ImportEveShipFitHash"; +import { EsfCargo, EsfDrone, EsfFit, EsfModule } from "../CurrentFitProvider/CurrentFitProvider"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const ConvertV2 = (fit: any) => { + const modules = fit.items + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((item: any): EsfModule | undefined => { + if (esiFlagToEsfSlot[item.flag] === undefined) return undefined; + + return { + typeId: item.type_id, + slot: esiFlagToEsfSlot[item.flag], + state: item.state ?? "Active", + charge: + item.charge === undefined + ? undefined + : { + typeId: item.charge.type_id, + }, + }; + }) + .filter((item: EsfModule | undefined) => item !== undefined) as EsfModule[]; + + const drones = fit.items + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((item: any): EsfDrone | undefined => { + if (item.flag !== 87) return undefined; + + return { + typeId: item.type_id, + states: { + Active: item.state !== "Passive" ? item.quantity : 0, + Passive: item.state === "Passive" ? item.quantity : 0, + }, + }; + }) + .filter((item: EsfDrone | undefined) => item !== undefined) as EsfDrone[]; + /* Drones can now be in the list twice, once for active and once for passive. Deduplicate. */ + const droneMap = new Map(); + drones.forEach((drone) => { + if (droneMap.has(drone.typeId)) { + droneMap.get(drone.typeId)!.states.Active += drone.states.Active; + droneMap.get(drone.typeId)!.states.Passive += drone.states.Passive; + } else { + droneMap.set(drone.typeId, drone); + } + }); + + const cargo = fit.items + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((item: any): EsfCargo | undefined => { + if (item.flag !== 5) return undefined; + + return { + typeId: item.type_id, + quantity: item.quantity, + }; + }) + .filter((item: EsfCargo | undefined) => item !== undefined) as EsfCargo[]; + + const newFit: EsfFit = { + name: fit.name, + shipTypeId: fit.ship_type_id, + description: fit.description, + modules, + drones, + cargo, + }; + return newFit; +}; diff --git a/src/providers/LocalFitsProvider/LocalFitsProvider.stories.tsx b/src/providers/LocalFitsProvider/LocalFitsProvider.stories.tsx index 52c1cfa..3f2c9bb 100644 --- a/src/providers/LocalFitsProvider/LocalFitsProvider.stories.tsx +++ b/src/providers/LocalFitsProvider/LocalFitsProvider.stories.tsx @@ -19,7 +19,7 @@ const TestStory = () => { {Object.values(localFits.fittings).map((fit) => { return (
- {fit.name} - {Object.keys(fit.items).length} items + {fit.name} - {Object.keys(fit.modules).length} modules
); })} diff --git a/src/providers/LocalFitsProvider/LocalFitsProvider.tsx b/src/providers/LocalFitsProvider/LocalFitsProvider.tsx index e7c5261..26e4a1b 100644 --- a/src/providers/LocalFitsProvider/LocalFitsProvider.tsx +++ b/src/providers/LocalFitsProvider/LocalFitsProvider.tsx @@ -3,6 +3,8 @@ import React from "react"; import { useLocalStorage } from "@/hooks/LocalStorage"; import { EsfFit } from "@/providers/CurrentFitProvider"; +import { ConvertV2 } from "./ConvertV2"; + interface LocalFits { fittings: EsfFit[]; addFit: (fit: EsfFit) => void; @@ -29,6 +31,27 @@ interface LocalFitsProps { */ export const LocalFitsProvider = (props: LocalFitsProps) => { const [localFitsValue, setLocalFitsValue] = useLocalStorage("fits", []); + const [firstLoad, setFirstLoad] = React.useState(true); + + if (firstLoad) { + setFirstLoad(false); + + let hasOldFits = false; + for (const index in localFitsValue) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const oldFit = localFitsValue[index] as any; + + /* If the fit has the field "ship_type_id", it is an old fit. Convert it to the new format. */ + if (oldFit.ship_type_id !== undefined) { + localFitsValue[index] = ConvertV2(oldFit); + hasOldFits = true; + } + } + + if (hasOldFits) { + setLocalFitsValue(localFitsValue); + } + } const addFit = React.useCallback( (fit: EsfFit) => { diff --git a/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx b/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx index ae95a23..fa0ad52 100644 --- a/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx +++ b/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx @@ -3,16 +3,11 @@ import React from "react"; import { fitArgType } from "../../../.storybook/fits"; -import { - CurrentCharacterProvider, - CurrentFitProvider, - DefaultCharactersProvider, - DogmaEngineProvider, - EsfFit, - EsiCharactersProvider, - EveDataProvider, - useCurrentFit, -} from "@/providers"; +import { CurrentFitProvider, EsfFit, useCurrentFit } from "../CurrentFitProvider"; +import { EveDataProvider } from "../EveDataProvider"; +import { DogmaEngineProvider } from "../DogmaEngineProvider"; +import { DefaultCharactersProvider, EsiCharactersProvider } from "../Characters"; +import { CurrentCharacterProvider } from "../CurrentCharacterProvider"; import { StatisticsProvider, useStatistics } from "./"; diff --git a/src/providers/StatisticsProvider/StatisticsProvider.tsx b/src/providers/StatisticsProvider/StatisticsProvider.tsx index 057d3b8..0dbaf05 100644 --- a/src/providers/StatisticsProvider/StatisticsProvider.tsx +++ b/src/providers/StatisticsProvider/StatisticsProvider.tsx @@ -1,62 +1,31 @@ import React from "react"; import { EveData, useEveData } from "@/providers/EveDataProvider"; -import { State, useCurrentFit } from "@/providers/CurrentFitProvider"; +import { useCurrentFit } from "@/providers/CurrentFitProvider"; import { useCurrentCharacter } from "@/providers/CurrentCharacterProvider"; -import { useDogmaEngine } from "@/providers/DogmaEngineProvider"; - -export interface StatisticsItemAttributeEffect { - operator: string; - penalty: boolean; - source: "Ship" | "Char" | "Structure" | "Target" | { Item?: number; Charge?: number; Skill?: number }; - source_category: string; - source_attribute_id: number; -} - -export interface StatisticsItemAttribute { - base_value: number; - value: number; - effects: StatisticsItemAttributeEffect[]; -} +import { Calculation, useDogmaEngine } from "@/providers/DogmaEngineProvider"; -export interface StatisticsItem { - type_id: number; - quantity: number; - flag: number; - charge: StatisticsItem | undefined; - state: State; - max_state: State; - attributes: Map; - effects: number[]; -} +const StatisticsSlotTypeEntries = ["High", "Medium", "Low", "SubSystem", "Rig", "Launcher", "Turret"] as const; +export type StatisticsSlotType = (typeof StatisticsSlotTypeEntries)[number]; -const StatisticsSlotEntries = ["hislot", "medslot", "lowslot", "subsystem", "rig", "launcher", "turret"] as const; -export type StatisticsSlotType = (typeof StatisticsSlotEntries)[number]; - -type StatisticsSlots = { +export type StatisticsSlots = { [key in StatisticsSlotType]: number; }; -interface Statistics { - hull: StatisticsItem; - items: StatisticsItem[]; - skills: StatisticsItem[]; - char: StatisticsItem; - structure: StatisticsItem; - target: StatisticsItem; - slots: StatisticsSlots; -} - const SlotAttributeMapping: Record = { - hislot: ["hiSlots", "hiSlotModifier"], - medslot: ["medSlots", "medSlotModifier"], - lowslot: ["lowSlots", "lowSlotModifier"], - subsystem: ["maxSubSystems", null], - rig: ["rigSlots", null], - launcher: ["launcherSlotsLeft", "launcherHardPointModifier"], - turret: ["turretSlotsLeft", "turretHardPointModifier"], + High: ["hiSlots", "hiSlotModifier"], + Medium: ["medSlots", "medSlotModifier"], + Low: ["lowSlots", "lowSlotModifier"], + SubSystem: ["maxSubSystems", null], + Rig: ["rigSlots", null], + Launcher: ["launcherSlotsLeft", "launcherHardPointModifier"], + Turret: ["turretSlotsLeft", "turretHardPointModifier"], }; +interface Statistics extends Calculation { + slots: StatisticsSlots; +} + const StatisticsContext = React.createContext(null); export const useStatistics = () => { @@ -69,14 +38,8 @@ interface StatisticsProps { } const CalculateSlots = (eveData: EveData, statistics: Statistics) => { - /* Set all slots to zero. */ - statistics.slots = StatisticsSlotEntries.reduce((acc, slot) => { - acc[slot] = 0; - return acc; - }, {} as StatisticsSlots); - /* Find the statistics of the hull. */ - for (const slot of StatisticsSlotEntries) { + for (const slot of StatisticsSlotTypeEntries) { const attributeId = SlotAttributeMapping[slot][0]; const attribute = statistics.hull.attributes.get(eveData.attributeMapping[attributeId]); @@ -84,9 +47,10 @@ const CalculateSlots = (eveData: EveData, statistics: Statistics) => { statistics.slots[slot] += value; } + /* Check if any items modify this value. */ for (const item of statistics.items) { - for (const slot of StatisticsSlotEntries) { + for (const slot of StatisticsSlotTypeEntries) { const attributeId = SlotAttributeMapping[slot][1]; if (attributeId === null) continue; @@ -96,6 +60,9 @@ const CalculateSlots = (eveData: EveData, statistics: Statistics) => { statistics.slots[slot] += value; } } + + /* EVE Online changed from 5 subsystems to 4, but the attributes aren't changed to match this. */ + if (statistics.slots.SubSystem === 5) statistics.slots.SubSystem = 4; }; /** @@ -119,7 +86,18 @@ export const StatisticsProvider = (props: StatisticsProps) => { return null; } - const statistics: Statistics = dogmaEngine.calculate(fit, skills); + const statistics: Statistics = { + ...dogmaEngine.calculate(fit, skills), + slots: { + High: 0, + Medium: 0, + Low: 0, + SubSystem: 0, + Rig: 0, + Launcher: 0, + Turret: 0, + }, + }; CalculateSlots(eveData, statistics); diff --git a/src/providers/StatisticsProvider/index.ts b/src/providers/StatisticsProvider/index.ts index 60970f1..6ecb754 100644 --- a/src/providers/StatisticsProvider/index.ts +++ b/src/providers/StatisticsProvider/index.ts @@ -1,7 +1,2 @@ export { StatisticsProvider, useStatistics } from "./StatisticsProvider"; -export type { - StatisticsItem, - StatisticsItemAttribute, - StatisticsItemAttributeEffect, - StatisticsSlotType, -} from "./StatisticsProvider"; +export type { StatisticsSlots, StatisticsSlotType } from "./StatisticsProvider";