feat: list hulls for creating new empty fits (#28)

This list can be searched and navigated pretty much like you can
in-game.
This commit is contained in:
Patric Stout
2023-11-27 20:07:33 +01:00
committed by GitHub
parent afc0cc3df5
commit 983917f5e6
11 changed files with 503 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ export interface TypeID {
groupID: number,
categoryID: number,
published: boolean,
factionID?: number,
marketGroupID?: number,
capacity?: number,
mass?: number,
@@ -26,6 +27,17 @@ export interface TypeID {
volume?: number,
}
export interface GroupID {
name: string,
categoryID: number,
published: boolean,
}
export interface MarketGroup {
name: string,
parentGroupID?: number,
}
export interface DogmaAttribute {
name: string
published: boolean,

View File

@@ -18,6 +18,8 @@ const TestEveData = () => {
return (
<div>
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}<br/>
GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}<br/>
MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}<br/>
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}<br/>
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}<br/>
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}<br/>

View File

@@ -1,6 +1,6 @@
import React from "react";
import { DogmaAttribute, DogmaEffect, TypeDogma, TypeID } from "./DataTypes";
import { DogmaAttribute, DogmaEffect, GroupID, MarketGroup, TypeDogma, TypeID } from "./DataTypes";
import { defaultDataUrl } from "../settings";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -11,6 +11,8 @@ import * as esf_pb2 from "./esf_pb2.js";
interface DogmaData {
loaded?: boolean;
typeIDs?: Record<string, TypeID>;
groupIDs?: Record<string, GroupID>;
marketGroups?: Record<string, MarketGroup>;
typeDogma?: Record<string, TypeDogma>;
dogmaEffects?: Record<string, DogmaEffect>;
dogmaAttributes?: Record<string, DogmaAttribute>;
@@ -37,6 +39,8 @@ async function fetchDataFile(dataUrl: string, name: string, pb2: any): Promise<o
function isLoaded(dogmaData: DogmaData): boolean | undefined {
if (dogmaData.typeIDs === undefined) return undefined;
if (dogmaData.groupIDs === undefined) return undefined;
if (dogmaData.marketGroups === undefined) return undefined;
if (dogmaData.typeDogma === undefined) return undefined;
if (dogmaData.dogmaEffects === undefined) return undefined;
if (dogmaData.dogmaAttributes === undefined) return undefined;
@@ -77,6 +81,8 @@ export const EveDataProvider = (props: DogmaDataProps) => {
}
fetchAndLoadDataFile("typeIDs", esf_pb2.esf.TypeIDs);
fetchAndLoadDataFile("groupIDs", esf_pb2.esf.GroupIDs);
fetchAndLoadDataFile("marketGroups", esf_pb2.esf.MarketGroups);
fetchAndLoadDataFile("typeDogma", esf_pb2.esf.TypeDogma);
fetchAndLoadDataFile("dogmaEffects", esf_pb2.esf.DogmaEffects);
fetchAndLoadDataFile("dogmaAttributes", esf_pb2.esf.DogmaAttributes);

View File

@@ -263,6 +263,7 @@ 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;
@@ -293,22 +294,26 @@ export const esf = $root.esf = (() => {
break;
}
case 5: {
m.marketGroupID = r.int32();
m.factionID = r.int32();
break;
}
case 6: {
m.capacity = r.float();
m.marketGroupID = r.int32();
break;
}
case 7: {
m.mass = r.float();
m.capacity = r.float();
break;
}
case 8: {
m.radius = r.float();
m.mass = r.float();
break;
}
case 9: {
m.radius = r.float();
break;
}
case 10: {
m.volume = r.float();
break;
}
@@ -334,6 +339,211 @@ export const esf = $root.esf = (() => {
return TypeIDs;
})();
esf.GroupIDs = (function() {
function GroupIDs(p) {
this.entries = {};
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
GroupIDs.prototype.entries = emptyObject;
GroupIDs.decode = async function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs(), k, value;
while (r.pos < c) {
if (r.need_data()) {
await r.fetch_data();
}
if (r.is_eof()) break;
var t = r.uint32();
switch (t >>> 3) {
case 1: {
if (m.entries === emptyObject)
m.entries = {};
var c2 = r.uint32() + r.pos;
k = 0;
value = null;
while (r.pos < c2) {
var tag2 = r.uint32();
switch (tag2 >>> 3) {
case 1:
k = r.int32();
break;
case 2:
value = $root.esf.GroupIDs.GroupID.decode(r, r.uint32());
break;
default:
r.skipType(tag2 & 7);
break;
}
}
m.entries[k] = value;
break;
}
default:
r.skipType(t & 7);
break;
}
}
return m;
};
GroupIDs.GroupID = (function() {
function GroupID(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
GroupID.prototype.name = "";
GroupID.prototype.categoryID = 0;
GroupID.prototype.published = false;
GroupID.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs.GroupID();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1: {
m.name = r.string();
break;
}
case 2: {
m.categoryID = r.int32();
break;
}
case 3: {
m.published = r.bool();
break;
}
default:
r.skipType(t & 7);
break;
}
}
if (!m.hasOwnProperty("name"))
throw Error("missing required 'name'", { instance: m });
if (!m.hasOwnProperty("categoryID"))
throw Error("missing required 'categoryID'", { instance: m });
if (!m.hasOwnProperty("published"))
throw Error("missing required 'published'", { instance: m });
return m;
};
return GroupID;
})();
return GroupIDs;
})();
esf.MarketGroups = (function() {
function MarketGroups(p) {
this.entries = {};
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
MarketGroups.prototype.entries = emptyObject;
MarketGroups.decode = async function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups(), k, value;
while (r.pos < c) {
if (r.need_data()) {
await r.fetch_data();
}
if (r.is_eof()) break;
var t = r.uint32();
switch (t >>> 3) {
case 1: {
if (m.entries === emptyObject)
m.entries = {};
var c2 = r.uint32() + r.pos;
k = 0;
value = null;
while (r.pos < c2) {
var tag2 = r.uint32();
switch (tag2 >>> 3) {
case 1:
k = r.int32();
break;
case 2:
value = $root.esf.MarketGroups.MarketGroup.decode(r, r.uint32());
break;
default:
r.skipType(tag2 & 7);
break;
}
}
m.entries[k] = value;
break;
}
default:
r.skipType(t & 7);
break;
}
}
return m;
};
MarketGroups.MarketGroup = (function() {
function MarketGroup(p) {
if (p)
for (var ks = Object.keys(p), i = 0; i < ks.length; ++i)
if (p[ks[i]] != null)
this[ks[i]] = p[ks[i]];
}
MarketGroup.prototype.name = "";
MarketGroup.prototype.parentGroupID = 0;
MarketGroup.decode = function decode(r, l) {
if (!(r instanceof $Reader))
r = $Reader.create(r);
var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups.MarketGroup();
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1: {
m.name = r.string();
break;
}
case 2: {
m.parentGroupID = r.int32();
break;
}
default:
r.skipType(t & 7);
break;
}
}
if (!m.hasOwnProperty("name"))
throw Error("missing required 'name'", { instance: m });
return m;
};
return MarketGroup;
})();
return MarketGroups;
})();
esf.DogmaAttributes = (function() {
function DogmaAttributes(p) {

View File

@@ -0,0 +1,86 @@
.listing {
background-color: #111111;
color: #c5c5c5;
font-size: 15px;
height: 100%;
position: relative;
width: 100%;
}
.listingContent {
height: calc(100% - 42px);
padding-right: 20px;
overflow-y: auto;
}
.header1, .header2, .header3 {
display: flex;
padding-left: 10px;
user-select: none;
}
.header1, .header2 {
height: 25px;
line-height: 25px;
}
.header1 {
background-color: #1d1d1d;
}
.header1:hover, .header2:hover, .header3:hover {
background-color: #4f4f4f;
}
.collapsed {
display: none;
}
.level1 {
padding-left: 20px;
}
.level2 {
padding-left: 40px;
}
.level3 {
padding-left: 60px;
}
.hull {
display: flex;
}
.hull > span {
display: inline-block;
height: 32px;
line-height: 32px;
margin-right: 6px;
}
.hull > span:nth-child(2) {
flex: 1;
}
.hull > span:last-child {
text-align: right;
}
.hull > span:last-child > img {
opacity: 0.5;
}
.hull > span:last-child > img:hover {
opacity: 1.0;
}
.hullSimulate {
cursor: pointer;
}
.topbar {
display: flex;
}
.topbar > input {
background-color: #1d1d1d;
color: #c5c5c5;
flex: 1;
height: 24px;
line-height: 24px;
margin: 6px;
padding-left: 6px;
}

View File

@@ -0,0 +1,34 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { EsiProvider } from '../EsiProvider';
import { HullListing } from './';
import { EveDataProvider } from '../EveDataProvider';
const meta: Meta<typeof HullListing> = {
component: HullListing,
tags: ['autodocs'],
title: 'Component/HullListing',
};
export default meta;
type Story = StoryObj<typeof HullListing>;
const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void }> = (Story) => {
return (
<EveDataProvider>
<EsiProvider>
<div style={{height: "400px"}}>
<Story />
</div>
</EsiProvider>
</EveDataProvider>
);
}
export const Default: Story = {
args: {
changeHull: (typeId: number) => console.log(`changeHull(${typeId})`),
},
decorators: [withEsiProvider],
};

View File

@@ -0,0 +1,144 @@
import clsx from "clsx";
import React from "react";
import { EveDataContext } from "../EveDataProvider";
import { Icon } from "../Icon";
import styles from "./HullListing.module.css";
interface ListingHulls {
[typeId: string]: string;
}
interface ListingGroup {
[raceName: string]: ListingHulls;
}
interface ListingGroups {
[groupName: string]: ListingGroup;
}
const factionIdToRace: Record<number, string> = {
500001: "Caldari",
500002: "Minmatar",
500003: "Amarr",
500004: "Gallente",
} as const;
const Hull = (props: { typeId: number, name: string, changeHull: (typeId: number) => void }) => {
const [expanded, setExpanded] = React.useState(false);
return <div>
<div className={clsx(styles.header3, styles.hull)} onClick={() => setExpanded((current) => !current)}>
<span>
<img src={`https://images.evetech.net/types/${props.typeId}/icon?size=32`} alt="" />
</span>
<span>
{props.name}
</span>
<span className={styles.hullSimulate} onClick={() => props.changeHull(props.typeId)}>
<Icon name="simulate" size={32} />
</span>
</div>
<div className={clsx(styles.level3, {[styles.collapsed]: !expanded})}>
</div>
</div>
}
const HullRace = (props: { name: string, entries: ListingHulls, changeHull: (typeId: number) => void }) => {
const [expanded, setExpanded] = React.useState(false);
if (props.entries === undefined) return null;
let children = <></>;
if (expanded) {
children = <>{Object.keys(props.entries).sort((a, b) => props.entries[a].localeCompare(props.entries[b])).map((typeId) => {
const name = props.entries[typeId];
return <Hull key={typeId} typeId={parseInt(typeId)} name={name} changeHull={props.changeHull} />
})}</>;
}
return <div>
<div className={styles.header2} onClick={() => setExpanded((current) => !current)}>
{props.name} [{Object.keys(props.entries).length}]
</div>
<div className={clsx(styles.level2, {[styles.collapsed]: !expanded})}>
{children}
</div>
</div>
}
const HullGroup = (props: { name: string, entries: ListingGroup, changeHull: (typeId: number) => void }) => {
const [expanded, setExpanded] = React.useState(false);
let children = <></>;
if (expanded) {
children = <>
<HullRace name="Amarr" entries={props.entries.Amarr} changeHull={props.changeHull} />
<HullRace name="Caldari" entries={props.entries.Caldari} changeHull={props.changeHull} />
<HullRace name="Gallente" entries={props.entries.Gallente} changeHull={props.changeHull} />
<HullRace name="Minmatar" entries={props.entries.Minmatar} changeHull={props.changeHull} />
<HullRace name="Non-Empire" entries={props.entries.NonEmpire} changeHull={props.changeHull} />
</>
}
return <div>
<div className={styles.header1} onClick={() => setExpanded((current) => !current)}>
{props.name}
</div>
<div className={clsx(styles.level1, {[styles.collapsed]: !expanded})}>
{children}
</div>
</div>
};
/**
* Show all the fittings for the current ESI character.
*/
export const HullListing = (props: { changeHull: (typeId: number) => void }) => {
const eveData = React.useContext(EveDataContext);
const [hullGroups, setHullGroups] = React.useState<ListingGroups>({});
const [search, setSearch] = React.useState<string>("");
React.useEffect(() => {
if (!eveData.loaded) return;
const newHullGroups: ListingGroups = {};
for (const typeId in eveData.typeIDs) {
const hull = eveData.typeIDs[typeId];
if (hull.categoryID !== 6) continue;
if (hull.marketGroupID === undefined) continue;
if (!hull.published) continue;
if (search !== "" && !hull.name.toLowerCase().includes(search.toLowerCase())) continue;
const group = eveData.groupIDs?.[hull.groupID]?.name ?? "Unknown Group";
const race = factionIdToRace[hull.factionID || 0] ?? "NonEmpire";
if (newHullGroups[group] === undefined) {
newHullGroups[group] = {};
}
if (newHullGroups[group][race] === undefined) {
newHullGroups[group][race] = {};
}
newHullGroups[group][race][typeId] = hull.name;
}
setHullGroups(newHullGroups);
}, [eveData, search]);
return <div className={styles.listing}>
<div className={styles.topbar}>
<input type="text" placeholder="Search" value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
<div className={styles.listingContent}>
{Object.keys(hullGroups).sort().map((groupName) => {
const groupData = hullGroups[groupName];
return <HullGroup key={groupName} name={groupName} entries={groupData} changeHull={props.changeHull} />
})}
</div>
</div>
};

1
src/HullListing/index.ts Normal file
View File

@@ -0,0 +1 @@
export { HullListing } from "./HullListing";

View File

@@ -20,6 +20,7 @@ const iconMapping = {
"shield-boost-rate": "texture/classes/fitting/statsicons/shieldboostrate.png",
"shield-hp": "texture/classes/fitting/statsicons/shieldhp.png",
"signature-radius": "texture/classes/fitting/statsicons/signatureradius.png",
"simulate": "texture/classes/fitting/iconsimulatorhover.png",
"thermal-resistance": "texture/classes/fitting/statsicons/thermalresistance.png",
"warp-speed": "texture/classes/fitting/statsicons/warpspeed.png",
} as const;

View File

@@ -5,6 +5,7 @@ export * from './EveDataProvider';
export * from './EveShipFitHash';
export * from './EveShipFitLink';
export * from './FormatEftToEsi';
export * from './HullListing';
export * from './Icon';
export * from './ShipAttribute';
export * from './ShipFit';

View File

@@ -1 +1 @@
export const defaultDataUrl = "https://data.eveship.fit/v1.2-20231115/";
export const defaultDataUrl = "https://data.eveship.fit/v4.0-20231115/";