chore!: create a more typed EsfFit (#130)

This commit is contained in:
Patric Stout
2024-05-20 21:00:46 +02:00
committed by GitHub
parent 955e884c16
commit e0701211e1
45 changed files with 1380 additions and 771 deletions

View File

@@ -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: "Killmail 117621358",
ship_type_id: 35833,
name: "Loki",
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 },
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",
description: "",
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 },
],
},
];

View File

@@ -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]);
};

View File

@@ -68,7 +68,7 @@
},
"peerDependencies": {
"@eveshipfit/data": "^9",
"@eveshipfit/dogma-engine": "^4",
"@eveshipfit/dogma-engine": "^5",
"react": "^18",
"react-dom": "^18"
},

View File

@@ -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() ?? [])];

View File

@@ -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<string, StatisticsItem[]> = {};
for (const drone of statistics.items.filter((item) => item.flag == 87)) {
const dronesGrouped: Record<string, CalculationItem[]> = {};
for (const drone of statistics.items.filter((item) => item.slot.type == "DroneBay")) {
const name = eveData.typeIDs?.[drone.type_id].name ?? "";
if (dronesGrouped[name] === undefined) {

View File

@@ -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 (
<>

View File

@@ -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<HTMLDivElement>) => void) => {
return (e: React.DragEvent<HTMLDivElement>) => {
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";
}
}

View File

@@ -72,7 +72,7 @@ const Hull = (props: { typeId: number; entry: ListingHull }) => {
return (
<TreeLeaf
key={`${fit.fit.ship_type_id}-${index}`}
key={`${fit.fit.shipTypeId}-${index}`}
level={4}
content={fit.fit.name}
onClick={() => fitManager.setFit(fit.fit)}
@@ -166,13 +166,13 @@ export const HullListing = () => {
const localFitsGrouped = React.useMemo(() => {
const grouped: Record<string, ListingFit[]> = {};
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<string, ListingFit[]> = {};
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]);

View File

@@ -14,7 +14,7 @@ export const Hull = () => {
return <></>;
}
const shipTypeId = currentFit.fit.ship_type_id;
const shipTypeId = currentFit.fit.shipTypeId;
if (shipTypeId === undefined) {
return <></>;
}

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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 }) => {
<Icon name="hardpoint-turret" size={16} />
</div>
</RingTopItem>
{Array.from({ length: slots.turret }, (_, i) => {
{Array.from({ length: slots.Turret }, (_, i) => {
turretSlotsUsed--;
return (
<RingTopItem key={i} rotation={-40 + i * 3} background>
@@ -85,7 +85,7 @@ export const ShipFit = (props: { withStats?: boolean }) => {
<Icon name="hardpoint-launcher" size={16} />
</div>
</RingTopItem>
{Array.from({ length: slots.launcher }, (_, i) => {
{Array.from({ length: slots.Launcher }, (_, i) => {
launcherSlotsUsed--;
return (
<RingTopItem key={i} rotation={39 - i * 3} background>
@@ -119,113 +119,113 @@ export const ShipFit = (props: { withStats?: boolean }) => {
)}
<RingTopItem rotation={-45} background>
<RadialMenu type="hislot" />
<RadialMenu type="High" />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 0}>
<Slot type="hislot" index={1} fittable={slots.hislot >= 1} main />
<Slot type="High" index={1} fittable={slots.High >= 1} main />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 1}>
<Slot type="hislot" index={2} fittable={slots.hislot >= 2} />
<Slot type="High" index={2} fittable={slots.High >= 2} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 2}>
<Slot type="hislot" index={3} fittable={slots.hislot >= 3} />
<Slot type="High" index={3} fittable={slots.High >= 3} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 3}>
<Slot type="hislot" index={4} fittable={slots.hislot >= 4} />
<Slot type="High" index={4} fittable={slots.High >= 4} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 4}>
<Slot type="hislot" index={5} fittable={slots.hislot >= 5} />
<Slot type="High" index={5} fittable={slots.High >= 5} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 5}>
<Slot type="hislot" index={6} fittable={slots.hislot >= 6} />
<Slot type="High" index={6} fittable={slots.High >= 6} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 6}>
<Slot type="hislot" index={7} fittable={slots.hislot >= 7} />
<Slot type="High" index={7} fittable={slots.High >= 7} />
</RingTopItem>
<RingTopItem rotation={-36.5 + (71 / 7) * 7}>
<Slot type="hislot" index={8} fittable={slots.hislot >= 8} />
<Slot type="High" index={8} fittable={slots.High >= 8} />
</RingTopItem>
<RingTopItem rotation={43} background>
<RadialMenu type="medslot" />
<RadialMenu type="Medium" />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 0}>
<Slot type="medslot" index={1} fittable={slots.medslot >= 1} />
<Slot type="Medium" index={1} fittable={slots.Medium >= 1} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 1}>
<Slot type="medslot" index={2} fittable={slots.medslot >= 2} />
<Slot type="Medium" index={2} fittable={slots.Medium >= 2} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 2}>
<Slot type="medslot" index={3} fittable={slots.medslot >= 3} />
<Slot type="Medium" index={3} fittable={slots.Medium >= 3} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 3}>
<Slot type="medslot" index={4} fittable={slots.medslot >= 4} />
<Slot type="Medium" index={4} fittable={slots.Medium >= 4} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 4}>
<Slot type="medslot" index={5} fittable={slots.medslot >= 5} />
<Slot type="Medium" index={5} fittable={slots.Medium >= 5} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 5}>
<Slot type="medslot" index={6} fittable={slots.medslot >= 6} />
<Slot type="Medium" index={6} fittable={slots.Medium >= 6} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 6}>
<Slot type="medslot" index={7} fittable={slots.medslot >= 7} />
<Slot type="Medium" index={7} fittable={slots.Medium >= 7} />
</RingTopItem>
<RingTopItem rotation={53 + (72 / 7) * 7}>
<Slot type="medslot" index={8} fittable={slots.medslot >= 8} />
<Slot type="Medium" index={8} fittable={slots.Medium >= 8} />
</RingTopItem>
<RingTopItem rotation={133} background>
<RadialMenu type="lowslot" />
<RadialMenu type="Low" />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 0}>
<Slot type="lowslot" index={1} fittable={slots.lowslot >= 1} />
<Slot type="Low" index={1} fittable={slots.Low >= 1} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 1}>
<Slot type="lowslot" index={2} fittable={slots.lowslot >= 2} />
<Slot type="Low" index={2} fittable={slots.Low >= 2} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 2}>
<Slot type="lowslot" index={3} fittable={slots.lowslot >= 3} />
<Slot type="Low" index={3} fittable={slots.Low >= 3} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 3}>
<Slot type="lowslot" index={4} fittable={slots.lowslot >= 4} />
<Slot type="Low" index={4} fittable={slots.Low >= 4} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 4}>
<Slot type="lowslot" index={5} fittable={slots.lowslot >= 5} />
<Slot type="Low" index={5} fittable={slots.Low >= 5} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 5}>
<Slot type="lowslot" index={6} fittable={slots.lowslot >= 6} />
<Slot type="Low" index={6} fittable={slots.Low >= 6} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 6}>
<Slot type="lowslot" index={7} fittable={slots.lowslot >= 7} />
<Slot type="Low" index={7} fittable={slots.Low >= 7} />
</RingTopItem>
<RingTopItem rotation={142 + (72 / 7) * 7}>
<Slot type="lowslot" index={8} fittable={slots.lowslot >= 8} />
<Slot type="Low" index={8} fittable={slots.Low >= 8} />
</RingTopItem>
<RingTopItem rotation={-74 + (21 / 2) * 0}>
<Slot type="rig" index={1} fittable={slots.rig >= 1} />
<Slot type="Rig" index={1} fittable={slots.Rig >= 1} />
</RingTopItem>
<RingTopItem rotation={-74 + (21 / 2) * 1}>
<Slot type="rig" index={2} fittable={slots.rig >= 2} />
<Slot type="Rig" index={2} fittable={slots.Rig >= 2} />
</RingTopItem>
<RingTopItem rotation={-74 + (21 / 2) * 2}>
<Slot type="rig" index={3} fittable={slots.rig >= 3} />
<Slot type="Rig" index={3} fittable={slots.Rig >= 3} />
</RingTopItem>
<RingTopItem rotation={-128 + (38 / 3) * 0}>
<Slot type="subsystem" index={1} fittable={slots.subsystem >= 1} />
<Slot type="SubSystem" index={1} fittable={slots.SubSystem >= 1} />
</RingTopItem>
<RingTopItem rotation={-128 + (38 / 3) * 1}>
<Slot type="subsystem" index={2} fittable={slots.subsystem >= 2} />
<Slot type="SubSystem" index={2} fittable={slots.SubSystem >= 2} />
</RingTopItem>
<RingTopItem rotation={-128 + (38 / 3) * 2}>
<Slot type="subsystem" index={3} fittable={slots.subsystem >= 3} />
<Slot type="SubSystem" index={3} fittable={slots.SubSystem >= 3} />
</RingTopItem>
<RingTopItem rotation={-128 + (38 / 3) * 3}>
<Slot type="subsystem" index={4} fittable={slots.subsystem >= 4} />
<Slot type="SubSystem" index={4} fittable={slots.SubSystem >= 4} />
</RingTopItem>
</RingTop>

View File

@@ -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<string, 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],
};
const stateRotation: Record<string, State[]> = {
const stateRotation: Record<string, EsfState[]> = {
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<SVGSVGElement, 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<HTMLElement, 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<SVGSVGElement, 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<SVGSVGElement, 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<HTMLDivElement>) => {
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<HTMLDivElement>) => {
@@ -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"
>
<use href="#slot" />
{props.fittable && esiItem && active && <use href="#slot-active" />}
{props.fittable && esiItem && !active && <use href="#slot-passive" />}
{props.fittable && module !== undefined && active && <use href="#slot-active" />}
{props.fittable && module !== undefined && !active && <use href="#slot-passive" />}
</svg>
</>
);
/* Not fittable and nothing fitted; no need to render the slot. */
if (esiItem === undefined && !props.fittable) {
if (module === undefined && !props.fittable) {
return (
<div className={styles.slotOuter} data-hasitem={false}>
<div className={styles.slot} data-state="Unavailable">
@@ -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 = (
<img
src={`https://images.evetech.net/types/${esiItem.charge.type_id}/icon?size=64`}
title={`${eveData.typeIDs[esiItem.type_id].name}\n${eveData.typeIDs[esiItem.charge.type_id].name}`}
src={`https://images.evetech.net/types/${module.charge.type_id}/icon?size=64`}
title={`${eveData.typeIDs[module.type_id].name}\n${eveData.typeIDs[module.charge.type_id].name}`}
draggable={true}
onDragStart={onDragStart}
/>
@@ -232,8 +229,8 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma
} else {
item = (
<img
src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`}
title={eveData.typeIDs[esiItem.type_id].name}
src={`https://images.evetech.net/types/${module.type_id}/icon?size=64`}
title={eveData.typeIDs[module.type_id].name}
draggable={true}
onDragStart={onDragStart}
/>
@@ -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 (
<div className={styles.slotOuter} data-hasitem={esiItem !== undefined}>
<div className={styles.slotOuter} data-hasitem={module !== undefined}>
<div className={styles.slot} onClick={cycleState} data-state={state} onDrop={onDragEnd} onDragOver={onDragOver}>
{svg}
<div className={imageStyle}>{item}</div>
</div>
<div className={styles.slotOptions}>
{esiItem?.charge !== undefined && (
{module?.charge !== undefined && (
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={unfitCharge}>
<title>Remove Charge</title>
<use href="#uncharge" />
@@ -285,7 +282,7 @@ export const Slot = (props: { type: string; index: number; fittable: boolean; ma
<title>Unfit Module</title>
<use href="#unfit" />
</svg>
{esiItem?.max_state !== "Passive" && (
{module?.max_state !== "Passive" && (
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={offlineState}>
<title>Put Offline</title>
<use href="#offline" />

View File

@@ -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 (
<>

View File

@@ -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;

View File

@@ -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<EsfSlotType, string> = {
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;
}
eft += moduleType.name;
if (module.quantity > 1) {
eft += ` x${module.quantity}`;
}
if (module.charge !== undefined) {
const chargeType = eveData.typeIDs[module.charge.type_id];
if (chargeType !== undefined) {
eft += `, ${chargeType.name}`;
}
}
eft += "\n";
const moduleType = eveData.typeIDs[module.typeId];
if (moduleType === undefined) {
eft += "[Empty " + slotToEft[slotType] + "]\n";
continue;
}
eft += moduleType.name;
if (module.charge !== undefined) {
const chargeType = eveData.typeIDs[module.charge.typeId];
if (chargeType !== undefined) {
eft += `, ${chargeType.name}`;
}
}
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;
};
}

View File

@@ -19,13 +19,19 @@ async function compress(str: string): Promise<string> {
}
async function encodeFit(fit: EsfFit): Promise<string> {
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));
}
/**

View File

@@ -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<string, 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],
};
/** Mapping between dogma effect IDs and slot types. */
const effectIdMapping: Record<number, string> = {
11: "lowslot",
13: "medslot",
12: "hislot",
2663: "rig",
3772: "subsystem",
const effectIdMapping: Record<number, EsfSlotType | "DroneBay"> = {
11: "Low",
13: "Medium",
12: "High",
2663: "Rig",
3772: "SubSystem",
};
const attributeIdMapping: Record<number, string> = {
1272: "droneBay",
const attributeIdMapping: Record<number, EsfSlotType | "DroneBay"> = {
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<string, number> = {
lowslot: 0,
medslot: 0,
hislot: 0,
rig: 0,
subsystem: 0,
droneBay: 0,
const slotIndex: Record<EsfSlotType, number> = {
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]++;

View File

@@ -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<typeof ImportEsiFitting> = {
component: ImportEsiFitting,
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof ImportEsiFitting>;
export const Default: Story = {
argTypes: {
esiFit: {
control: "select",
options: Object.keys(esiFits),
mapping: esiFits,
},
},
decorators: [
(Story) => (
<EveDataProvider>
<Story />
</EveDataProvider>
),
],
render: (args) => <ImportEsiFitting {...args} />,
};

View File

@@ -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 <div>No fit selected.</div>;
}
const fit = importEsiFitting(props.esiFit);
return <pre>{JSON.stringify(fit, null, 2)}</pre>;
};

View File

@@ -0,0 +1,2 @@
export { useImportEsiFitting } from "./ImportEsiFitting";
export type { EsiFit } from "./ImportEsiFitting";

View File

@@ -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<EsfFit | null> => {
const eft = await decompress(eftCompressed);
return importEft(eft);
};
}

View File

@@ -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<EsfFit | null> {
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.
};
}

View File

@@ -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<EsfFit | null> {
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<number, EsfDrone>();
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,
};
}

View File

@@ -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<EsfFit | null> {
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,
};
}

View File

@@ -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<EsfFit | null> => {
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,
};
};
}

View File

@@ -0,0 +1,15 @@
export async function decompress(base64compressedBytes: string): Promise<string> {
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;
}

View File

@@ -0,0 +1,35 @@
import { EsfSlot } from "@/providers/CurrentFitProvider";
export const esiFlagToEsfSlot: Record<number, EsfSlot> = {
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 },
};

View File

@@ -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<string> {
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<EsfFit | null> {
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<EsfFit | null> {
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<EsfFit | null> => {
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<EsfFit | null> => {
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<EsfFit | null | undefined>(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 <div>Select a fit hash.</div>;
}
return (
<div>

View File

@@ -1 +1,2 @@
export { useImportEveShipFitHash } from "./ImportEveShipFitHash";
export { esiFlagToEsfSlot } from "./EsiFlags";

View File

@@ -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";

View File

@@ -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<string, Character>) => {
return {
...oldEsiCharacters,
@@ -155,7 +165,7 @@ export const EsiCharactersProvider = (props: EsiProps) => {
};
});
},
[setEsiCharacters, ensureAccessToken, eveData],
[setEsiCharacters, importEsiFitting, ensureAccessToken, eveData],
);
if (firstLoad) {

View File

@@ -1,6 +1,6 @@
import { EsfFit } from "@/providers/CurrentFitProvider";
import { EsiFit } from "@/hooks/ImportEsiFitting";
export async function getCharFittings(characterId: string, accessToken: string): Promise<EsfFit[] | undefined> {
export async function getCharFittings(characterId: string, accessToken: string): Promise<EsiFit[] | undefined> {
let response;
try {
response = await fetch(`https://esi.evetech.net/v1/characters/${characterId}/fittings/`, {

View File

@@ -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 {

View File

@@ -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";

View File

@@ -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<number, CalculationItemAttribute>;
effects: number[];
}
export interface Calculation {
hull: CalculationItem;
items: CalculationItem[];
skills: CalculationItem[];
char: CalculationItem;
structure: CalculationItem;
target: CalculationItem;
}

View File

@@ -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<DogmaEngine | null>(null);
@@ -60,7 +62,9 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => {
const [firstLoad, setFirstLoad] = React.useState(true);
const [dogmaEngine, setDogmaEngine] = React.useState<DogmaEngine | null>(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 <DogmaEngineContext.Provider value={contextValue}>{props.children}</DogmaEngineContext.Provider>;

View File

@@ -1 +1,10 @@
export { DogmaEngineProvider, useDogmaEngine } from "./DogmaEngineProvider";
export type {
Calculation,
CalculationItem,
CalculationItemAttribute,
CalculationItemAttributeEffect,
CalculationSlot,
CalculationSlotType,
CalculationState,
} from "./DataTypes";

View File

@@ -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<StatisticsSlotType, number> = {
hislot: 27,
medslot: 19,
lowslot: 11,
subsystem: 125,
rig: 92,
launcher: 0,
turret: 0,
};
const FitManagerContext = React.createContext<FitManager>({
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 modules = [...oldFit.modules];
const fromItemIndex = newItems.findIndex((item) => item.flag === flagA);
const fromItem = newItems[fromItemIndex];
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);
const toItemIndex = newItems.findIndex((item) => item.flag === flagB);
const toItem = newItems[toItemIndex];
if (moduleA !== undefined) {
moduleA.slot.index = slotB.index;
}
fromItem.flag = flagB;
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);
const drones = oldFit.drones.filter((item) => item.typeId !== typeId);
const passive = count - active;
/* Add the active drones. */
if (active > 0) {
newItems.push({
flag: 87,
type_id: typeId,
quantity: active,
state: "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),
};
});
},

View File

@@ -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<number, EsfDrone>();
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;
};

View File

@@ -19,7 +19,7 @@ const TestStory = () => {
{Object.values(localFits.fittings).map((fit) => {
return (
<div key={fit.name}>
{fit.name} - {Object.keys(fit.items).length} items
{fit.name} - {Object.keys(fit.modules).length} modules
</div>
);
})}

View File

@@ -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<EsfFit[]>("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) => {

View File

@@ -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 "./";

View File

@@ -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";
import { Calculation, 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;
}
const StatisticsSlotTypeEntries = ["High", "Medium", "Low", "SubSystem", "Rig", "Launcher", "Turret"] as const;
export type StatisticsSlotType = (typeof StatisticsSlotTypeEntries)[number];
export interface StatisticsItemAttribute {
base_value: number;
value: number;
effects: StatisticsItemAttributeEffect[];
}
export interface StatisticsItem {
type_id: number;
quantity: number;
flag: number;
charge: StatisticsItem | undefined;
state: State;
max_state: State;
attributes: Map<number, StatisticsItemAttribute>;
effects: 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;
const SlotAttributeMapping: Record<StatisticsSlotType, [string, string | null]> = {
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 SlotAttributeMapping: Record<StatisticsSlotType, [string, string | null]> = {
hislot: ["hiSlots", "hiSlotModifier"],
medslot: ["medSlots", "medSlotModifier"],
lowslot: ["lowSlots", "lowSlotModifier"],
subsystem: ["maxSubSystems", null],
rig: ["rigSlots", null],
launcher: ["launcherSlotsLeft", "launcherHardPointModifier"],
turret: ["turretSlotsLeft", "turretHardPointModifier"],
};
const StatisticsContext = React.createContext<Statistics | null>(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);

View File

@@ -1,7 +1,2 @@
export { StatisticsProvider, useStatistics } from "./StatisticsProvider";
export type {
StatisticsItem,
StatisticsItemAttribute,
StatisticsItemAttributeEffect,
StatisticsSlotType,
} from "./StatisticsProvider";
export type { StatisticsSlots, StatisticsSlotType } from "./StatisticsProvider";