diff --git a/src/EveDataProvider/DataTypes.tsx b/src/EveDataProvider/DataTypes.tsx index d0266fc..e84993b 100644 --- a/src/EveDataProvider/DataTypes.tsx +++ b/src/EveDataProvider/DataTypes.tsx @@ -21,6 +21,7 @@ export interface TypeID { published: boolean, factionID?: number, marketGroupID?: number, + metaGroupID?: number, capacity?: number, mass?: number, radius?: number, @@ -36,6 +37,7 @@ export interface GroupID { export interface MarketGroup { name: string, parentGroupID?: number, + iconID?: number, } export interface DogmaAttribute { diff --git a/src/EveDataProvider/esf_pb2.js b/src/EveDataProvider/esf_pb2.js index e73575e..5723772 100644 --- a/src/EveDataProvider/esf_pb2.js +++ b/src/EveDataProvider/esf_pb2.js @@ -263,12 +263,13 @@ export const esf = $root.esf = (() => { TypeID.prototype.groupID = 0; TypeID.prototype.categoryID = 0; TypeID.prototype.published = false; - TypeID.prototype.factionID = 0; - TypeID.prototype.marketGroupID = 0; - TypeID.prototype.capacity = 0; - TypeID.prototype.mass = 0; - TypeID.prototype.radius = 0; - TypeID.prototype.volume = 0; + TypeID.prototype.factionID = undefined; + TypeID.prototype.marketGroupID = undefined; + TypeID.prototype.metaGroupID = undefined; + TypeID.prototype.capacity = undefined; + TypeID.prototype.mass = undefined; + TypeID.prototype.radius = undefined; + TypeID.prototype.volume = undefined; TypeID.decode = function decode(r, l) { if (!(r instanceof $Reader)) @@ -302,18 +303,22 @@ export const esf = $root.esf = (() => { break; } case 7: { - m.capacity = r.float(); + m.metaGroupID = r.int32(); break; } case 8: { - m.mass = r.float(); + m.capacity = r.float(); break; } case 9: { - m.radius = r.float(); + m.mass = r.float(); break; } case 10: { + m.radius = r.float(); + break; + } + case 11: { m.volume = r.float(); break; } @@ -511,7 +516,8 @@ export const esf = $root.esf = (() => { } MarketGroup.prototype.name = ""; - MarketGroup.prototype.parentGroupID = 0; + MarketGroup.prototype.parentGroupID = undefined; + MarketGroup.prototype.iconID = undefined; MarketGroup.decode = function decode(r, l) { if (!(r instanceof $Reader)) @@ -528,6 +534,10 @@ export const esf = $root.esf = (() => { m.parentGroupID = r.int32(); break; } + case 3: { + m.iconID = r.int32(); + break; + } default: r.skipType(t & 7); break; @@ -738,14 +748,14 @@ export const esf = $root.esf = (() => { DogmaEffect.prototype.isWarpSafe = false; DogmaEffect.prototype.propulsionChance = false; DogmaEffect.prototype.rangeChance = false; - DogmaEffect.prototype.dischargeAttributeID = 0; - DogmaEffect.prototype.durationAttributeID = 0; - DogmaEffect.prototype.rangeAttributeID = 0; - DogmaEffect.prototype.falloffAttributeID = 0; - DogmaEffect.prototype.trackingSpeedAttributeID = 0; - DogmaEffect.prototype.fittingUsageChanceAttributeID = 0; - DogmaEffect.prototype.resistanceAttributeID = 0; - DogmaEffect.prototype.modifierInfo = emptyArray; + DogmaEffect.prototype.dischargeAttributeID = undefined; + DogmaEffect.prototype.durationAttributeID = undefined; + DogmaEffect.prototype.rangeAttributeID = undefined; + DogmaEffect.prototype.falloffAttributeID = undefined; + DogmaEffect.prototype.trackingSpeedAttributeID = undefined; + DogmaEffect.prototype.fittingUsageChanceAttributeID = undefined; + DogmaEffect.prototype.resistanceAttributeID = undefined; + DogmaEffect.prototype.modifierInfo = undefined; DogmaEffect.decode = function decode(r, l) { if (!(r instanceof $Reader)) @@ -855,11 +865,11 @@ export const esf = $root.esf = (() => { ModifierInfo.prototype.domain = 0; ModifierInfo.prototype.func = 0; - ModifierInfo.prototype.modifiedAttributeID = 0; - ModifierInfo.prototype.modifyingAttributeID = 0; - ModifierInfo.prototype.operation = 0; - ModifierInfo.prototype.groupID = 0; - ModifierInfo.prototype.skillTypeID = 0; + ModifierInfo.prototype.modifiedAttributeID = undefined; + ModifierInfo.prototype.modifyingAttributeID = undefined; + ModifierInfo.prototype.operation = undefined; + ModifierInfo.prototype.groupID = undefined; + ModifierInfo.prototype.skillTypeID = undefined; ModifierInfo.decode = function decode(r, l) { if (!(r instanceof $Reader)) diff --git a/src/HardwareListing/HardwareListing.module.css b/src/HardwareListing/HardwareListing.module.css new file mode 100644 index 0000000..ef9c6e3 --- /dev/null +++ b/src/HardwareListing/HardwareListing.module.css @@ -0,0 +1,57 @@ +.listing { + background-color: #111111; + color: #c5c5c5; + font-size: 15px; + height: 100%; + position: relative; + width: 100%; +} + +.listingContent { + height: calc(100% - 42px - 32px - 5px); + overflow-y: auto; + padding-right: 20px; +} + +.topbar { + display: flex; +} + +.topbar > input { + background-color: #1d1d1d; + color: #c5c5c5; + flex: 1; + height: 24px; + line-height: 24px; + margin: 6px 0; + padding-left: 6px; +} + +.filter { + display: flex; + margin-bottom: 5px; +} +.filter > span { + border-radius: 5px; + cursor: pointer; + display: inline-block; + height: 32px; + line-height: 32px; + text-align: center; + user-select: none; + width: 32px; +} +.filter > span:hover { + background-color: #4f4f4f; +} +.filter > span.selected { + background-color: #7a7a7a; +} + +.filter > span.disabled { + color: #7a7a7a; + cursor: default; +} +.filter > span.disabled:hover { + background-color: #111111; +} diff --git a/src/HardwareListing/HardwareListing.stories.tsx b/src/HardwareListing/HardwareListing.stories.tsx new file mode 100644 index 0000000..2d77d20 --- /dev/null +++ b/src/HardwareListing/HardwareListing.stories.tsx @@ -0,0 +1,29 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EveDataProvider } from '../EveDataProvider'; + +import { HardwareListing } from './'; + +const meta: Meta = { + component: HardwareListing, + tags: ['autodocs'], + title: 'Component/HardwareListing', +}; + +export default meta; +type Story = StoryObj; + +const withEveDataProvider: Decorator> = (Story) => { + return ( + +
+ +
+
+ ); +} + +export const Default: Story = { + decorators: [withEveDataProvider], +}; diff --git a/src/HardwareListing/HardwareListing.tsx b/src/HardwareListing/HardwareListing.tsx new file mode 100644 index 0000000..13e9690 --- /dev/null +++ b/src/HardwareListing/HardwareListing.tsx @@ -0,0 +1,219 @@ +import clsx from "clsx"; +import React from "react"; + +import { defaultDataUrl } from "../settings"; +import { EveDataContext } from "../EveDataProvider"; +import { Icon } from "../Icon"; +import { ShipSnapshotContext, ShipSnapshotSlotsType } from "../ShipSnapshotProvider"; +import { TreeListing, TreeHeader, TreeLeaf } from "../TreeListing"; + +import styles from "./HardwareListing.module.css"; + +interface ListingItem { + name: string; + meta: number; + typeId: number; + slotType: ShipSnapshotSlotsType | "dronebay"; +} + +interface ListingGroup { + name: string; + meta: number; + iconID?: number; + groups: Record; + items: ListingItem[]; +} + +const ModuleGroup = (props: { level: number, group: ListingGroup }) => { + const shipSnapShot = React.useContext(ShipSnapshotContext); + + const getChildren = React.useCallback(() => { + return <> + {props.group.items.sort((a, b) => a.meta - b.meta || a.name.localeCompare(b.name)).map((item) => { + return shipSnapShot.addModule(item.typeId, item.slotType)} />; + })} + {Object.keys(props.group.groups).sort((a, b) => props.group.groups[a].meta - props.group.groups[b].meta || props.group.groups[a].name.localeCompare(props.group.groups[b].name)).map((groupId) => { + return + })} + ; + }, [props, shipSnapShot]); + + const header = ; + return ; +}; + +/** + * Show all the modules you can fit to a ship. + */ +export const HardwareListing = () => { + const eveData = React.useContext(EveDataContext); + + const [moduleGroups, setModuleGroups] = React.useState({ + name: "Modules", + meta: 0, + groups: {}, + items: [], + }); + const [search, setSearch] = React.useState(""); + const [filter, setFilter] = React.useState({ + lowslot: false, + medslot: false, + hislot: false, + rig_subsystem: false, + drone: false, + }); + + React.useEffect(() => { + if (!eveData.loaded) return; + + const newModuleGroups: ListingGroup = { + name: "Modules", + meta: 0, + groups: {}, + items: [], + }; + + for (const typeId in eveData.typeIDs) { + const module = eveData.typeIDs[typeId]; + /* Modules (7), Drones (18), Subsystems (32), and Structures (66) */ + if (module.categoryID !== 7 && module.categoryID !== 18 && module.categoryID !== 32 && module.categoryID !== 66) continue; + if (module.marketGroupID === undefined) continue; + if (!module.published) continue; + + let slotType: ShipSnapshotSlotsType | "dronebay" | undefined = eveData.typeDogma?.[typeId]?.dogmaEffects.map((effect) => { + switch (effect.effectID) { + case 11: return "lowslot"; + case 13: return "medslot"; + case 12: return "hislot"; + case 2663: return "rig"; + case 3772: return "subsystem"; + } + }).filter((slot) => slot !== undefined)[0]; + if (module.categoryID === 18) { + slotType = "dronebay"; + } + + if (slotType === undefined) continue; + + if (filter.lowslot && slotType !== "lowslot") continue; + if (filter.medslot && slotType !== "medslot") continue; + if (filter.hislot && slotType !== "hislot") continue; + if (filter.rig_subsystem && slotType !== "rig" && slotType !== "subsystem") continue; + if (filter.drone && module.categoryID !== 18) continue; + + if (search !== "" && !module.name.toLowerCase().includes(search.toLowerCase())) continue; + + const marketGroups: number[] = []; + + switch (module.metaGroupID) { + case 3: // Storyline + case 4: // Faction + marketGroups.push(-1); + break; + + case 5: // Officer + marketGroups.push(-2); + break; + + case 6: // Deadspace + marketGroups.push(-3); + break; + } + + /* Construct the tree of the module's market groups. */ + let marketGroup: number | undefined = module.marketGroupID; + while (marketGroup !== undefined) { + marketGroups.push(marketGroup); + marketGroup = eveData.marketGroups?.[marketGroup].parentGroupID; + } + + /* Remove the root group. */ + marketGroups.pop(); + /* Put Drones and Structures in their own group. */ + if (module.categoryID === 18) marketGroups.push(157); + if (module.categoryID === 66) marketGroups.push(477); + + /* Build up the tree till the find the leaf node. */ + let groupTree = newModuleGroups; + for (const group of marketGroups.reverse()) { + if (groupTree.groups[group] === undefined) { + let name; + let meta; + let iconID = undefined; + switch (group) { + case -1: + name = "Faction & Storyline"; + iconID = 24146; + meta = 3; + break; + + case -2: + name = "Officer"; + iconID = 24149; + meta = 5; + break; + + case -3: + name = "Deadspace"; + iconID = 24148; + meta = 6; + break; + + default: + name = eveData.marketGroups?.[group].name ?? "Unknown group"; + meta = 1; + iconID = eveData.marketGroups?.[group].iconID; + break; + } + + groupTree.groups[group] = { + name, + meta, + iconID, + groups: {}, + items: [], + }; + } + + groupTree = groupTree.groups[group]; + } + + groupTree.items.push({ + name: module.name, + meta: module.metaGroupID ?? 0, + typeId: parseInt(typeId), + slotType, + }); + } + + setModuleGroups(newModuleGroups); + }, [eveData, search, filter]); + + return
+
+ setSearch(e.target.value)} /> +
+
+ setFilter({...filter, lowslot: !filter.lowslot})}> + + + setFilter({...filter, medslot: !filter.medslot})}> + + + setFilter({...filter, hislot: !filter.hislot})}> + + + setFilter({...filter, rig_subsystem: !filter.rig_subsystem})}> + + + setFilter({...filter, drone: !filter.drone})}> + + +
+
+ {Object.keys(moduleGroups.groups).sort((a, b) => moduleGroups.groups[a].name.localeCompare(moduleGroups.groups[b].name)).map((groupId) => { + return + })} +
+
+}; diff --git a/src/HardwareListing/index.ts b/src/HardwareListing/index.ts new file mode 100644 index 0000000..54f5fbf --- /dev/null +++ b/src/HardwareListing/index.ts @@ -0,0 +1 @@ +export { HardwareListing } from "./HardwareListing"; diff --git a/src/Icon/Icon.tsx b/src/Icon/Icon.tsx index bfa1e8e..128185f 100644 --- a/src/Icon/Icon.tsx +++ b/src/Icon/Icon.tsx @@ -11,8 +11,13 @@ const iconMapping = { "fitting-alliance": "texture/classes/fitting/taballiancefits.png", "fitting-character": "texture/windowicons/member.png", "fitting-corporation": "texture/windowicons/corporation.png", + "fitting-drones": "texture/classes/fitting/filtericondrones.png", + "fitting-hislot": "texture/classes/fitting/filtericonhighslot.png", "fitting-hull": "texture/classes/fitting/tabfittings.png", "fitting-local": "texture/windowicons/note.png", + "fitting-lowslot": "texture/classes/fitting/filtericonlowslot.png", + "fitting-medslot": "texture/classes/fitting/filtericonmediumslot.png", + "fitting-rig-subsystem": "texture/classes/fitting/filtericonrigslot.png", "hull-hp": "texture/classes/fitting/statsicons/structurehp.png", "hull-repair-rate": "texture/classes/fitting/statsicons/hullrepairrate.png", "inertia-modifier": "texture/classes/fitting/statsicons/inertiamodifier.png", diff --git a/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx index 57ea897..bdefb1f 100644 --- a/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx +++ b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx @@ -57,6 +57,7 @@ interface ShipSnapshot { fit?: EsiFit; + addModule: (typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => void; changeHull: (typeId: number) => void; changeFit: (fit: EsiFit) => void; setItemState: (flag: number, state: string) => void; @@ -71,11 +72,20 @@ export const ShipSnapshotContext = React.createContext({ "subsystem": 0, "rig": 0, }, + addModule: () => {}, changeHull: () => {}, changeFit: () => {}, setItemState: () => {}, }); +const slotStart: Record = { + "hislot": 27, + "medslot": 19, + "lowslot": 11, + "subsystem": 125, + "rig": 92, +}; + export interface ShipSnapshotProps { /** Children that can use this provider. */ children: React.ReactNode; @@ -99,6 +109,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { "subsystem": 0, "rig": 0, }, + addModule: () => {}, changeHull: () => {}, changeFit: () => {}, setItemState: () => {}, @@ -107,8 +118,6 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { const dogmaEngine = React.useContext(DogmaEngineContext); const setItemState = React.useCallback((flag: number, state: string) => { - if (currentFit === undefined) return; - setCurrentFit((oldFit: EsiFit | undefined) => { if (oldFit === undefined) return undefined; @@ -126,7 +135,42 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { }), }; }) - }, [currentFit]); + }, []); + + const addModule = React.useCallback((typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => { + setCurrentFit((oldFit: EsiFit | undefined) => { + if (oldFit === undefined) return undefined; + + let flag = 0; + + /* Find the first free slot for that slot-type. */ + if (slot !== "dronebay") { + for (let i = slotStart[slot]; i < slotStart[slot] + shipSnapshot.slots[slot]; i++) { + if (oldFit.items.find((item) => item.flag === i) !== undefined) continue; + + flag = i; + break; + } + } else { + flag = 87; + } + + /* Couldn't find a free slot. */ + if (flag === 0) return oldFit; + + return { + ...oldFit, + items: [ + ...oldFit.items, + { + flag: flag, + type_id: typeId, + quantity: 1, + } + ], + }; + }) + }, [shipSnapshot.slots]); const changeHull = React.useCallback((typeId: number) => { setCurrentFit({ @@ -137,6 +181,16 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { }) }, []); + React.useEffect(() => { + setShipSnapshot((oldSnapshot) => ({ + ...oldSnapshot, + addModule, + changeHull, + changeFit: setCurrentFit, + setItemState, + })); + }, [addModule, changeHull, setItemState]); + React.useEffect(() => { if (!dogmaEngine.loaded) return; if (currentFit === undefined || props.skills === undefined) return; @@ -164,17 +218,17 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { slots.lowslot += item.attributes.get(eveData?.attributeMapping?.lowSlotModifier || 0)?.value || 0; } - setShipSnapshot({ - loaded: true, - hull: snapshot.hull, - items: snapshot.items, - slots, - fit: currentFit, - changeHull, - changeFit: setCurrentFit, - setItemState, + setShipSnapshot((oldSnapshot) => { + return { + ...oldSnapshot, + loaded: true, + hull: snapshot.hull, + items: snapshot.items, + slots, + fit: currentFit, + }; }); - }, [eveData, dogmaEngine, currentFit, props.skills, changeHull, setItemState]); + }, [eveData, dogmaEngine, currentFit, props.skills]); React.useEffect(() => { setCurrentFit(props.fit); diff --git a/src/TreeListing/TreeListing.module.css b/src/TreeListing/TreeListing.module.css index d099852..1ab579c 100644 --- a/src/TreeListing/TreeListing.module.css +++ b/src/TreeListing/TreeListing.module.css @@ -1,8 +1,8 @@ .header { display: flex; height: var(--height); - padding: 2px 0; line-height: var(--height); + padding: 2px 0; user-select: none; } .header > span { @@ -18,12 +18,13 @@ .headerText { flex: 1; + white-space: nowrap; } .headerAction { cursor: pointer; - opacity: 0.5; margin-right: 4px; + opacity: 0.5; } .headerAction:hover { opacity: 1.0; diff --git a/src/index.ts b/src/index.ts index 77e828e..2e95e7b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './EveDataProvider'; export * from './EveShipFitHash'; export * from './EveShipFitLink'; export * from './FormatEftToEsi'; +export * from './HardwareListing'; export * from './HullListing'; export * from './Icon'; export * from './ShipAttribute'; diff --git a/src/settings.ts b/src/settings.ts index c7ef7e9..ba7ee6a 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1 +1 @@ -export const defaultDataUrl = "https://data.eveship.fit/v4.1-20231115/"; +export const defaultDataUrl = "https://data.eveship.fit/v6.1-20231115/";