diff --git a/.babelrc.json b/.babelrc.json new file mode 100644 index 0000000..9164ce7 --- /dev/null +++ b/.babelrc.json @@ -0,0 +1,18 @@ +{ + "sourceType": "unambiguous", + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "chrome": 100, + "safari": 15, + "firefox": 91 + } + } + ], + "@babel/preset-typescript", + "@babel/preset-react" + ], + "plugins": [] +} diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..d3fd280 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,43 @@ +const path = require("path") + +module.exports = { + env: { + browser: true, + es6: true, + }, + extends: [ + "airbnb-typescript", + "airbnb/hooks", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:react/jsx-runtime", + "plugin:react/recommended", + "plugin:storybook/recommended", + "prettier", + ], + plugins: [ + "@typescript-eslint", + "import", + "prettier", + "react", + ], + "settings": { + "react": { + "version": "detect" + } + }, + rules: { + "newline-per-chained-call": "off", + "react/jsx-pascal-case": "off", + "react/require-default-props": "off", + }, + parserOptions: { + project: "./tsconfig.json", + }, + ignorePatterns: [ + "src/EveDataProvider/esf_pb2.js", + "src/EveDataProvider/protobuf.js", + ], + overrides: [], +} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..2223ae1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,42 @@ +name: Release + +on: + release: + types: + - published + pull_request_target: + branches: + - main + +jobs: + storybook: + name: Publish storybook + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install NodeJS + uses: actions/setup-node@v3 + with: + registry-url: https://npm.pkg.github.com + scope: "@eveshipfit" + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: npm install + + - name: Build storybook + run: npm run build-storybook + + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@v1 + id: pages + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ vars.CLOUDFLARE_PROJECT_NAME }} + directory: storybook-static + branch: ${{ github.event_name == 'push' && github.ref_name || github.event_name == 'repository_dispatch' && 'main' || format('pr/{0}', github.event.pull_request.number) }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..87985ba --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,43 @@ +name: Release + +on: + release: + types: + - published + +jobs: + registries: + name: Publish to GitHub NPM + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install NodeJS + uses: actions/setup-node@v3 + with: + registry-url: https://npm.pkg.github.com + scope: "@eveshipfit" + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: npm install + + - name: Set version + run: | + # Remove the "v" from the version. + VERSION=$(echo ${{ github.ref_name }} | cut -b2-) + echo "Version: ${VERSION}" + + sed -i 's/version = "0.0.0-git"/version = "'${VERSION}'"/' package.json + + - name: Create NPM package + run: npm run build + + - uses: JS-DevTools/npm-publish@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + registry: "https://npm.pkg.github.com" + package: package.json diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..7ed4d7d --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,32 @@ +name: Testing + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + testing: + name: Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install NodeJS + uses: actions/setup-node@v3 + with: + registry-url: https://npm.pkg.github.com + scope: "@eveshipfit" + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Install dependencies + run: npm install + + - name: Run linter + run: npm run lint diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b0ddc3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/dist/ +/node_modules/ +/package-lock.json +/storybook-static/ +/.eslintcache diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..5cf5267 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@eveshipfit:registry=https://npm.pkg.github.com diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..8d3eb3d --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,26 @@ +import type { StorybookConfig } from "@storybook/react-webpack5"; + +const config: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/react-webpack5", + options: {}, + }, + docs: { + autodocs: "tag", + }, + webpackFinal: async (config: any) => { + return { + ...config, + experiments: { + asyncWebAssembly: true + } + } + }, +}; +export default config; diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..ff58bbd --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,15 @@ +import type { Preview } from "@storybook/react"; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68b1edc --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 TrueBrain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..63c22b0 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# React Component library for EVEShip.fit + +This repository contains all the components used to build [EVEShip.fit](https://eveship.fit). + +## Components + +Via [Storybook](https://react.eveship.fit) you can view all the components, their description, and how to use them. + +## Embedding in your own application + +This library can (freely) be used to visualize an EVE Fit in your own (React) application, by using the components you like most. + +Important to note is that by default the data-files are downloaded from `https://data.eveship.fit/`. +But this URL is protected by a CORS, to avoid unrealistic cost on our side. + +If you use this library yourself, you would have to host these files yourself too. +You can define the URL they are hosted on in the `EveDataProvider` via `dataUrl`. diff --git a/package.json b/package.json new file mode 100644 index 0000000..2b0cc23 --- /dev/null +++ b/package.json @@ -0,0 +1,72 @@ +{ + "name": "@eveshipfit/react", + "publishConfig": { + "registry": "https://npm.pkg.github.com/EVEShipFit" + }, + "version": "0.0.0-git", + "description": "React component library to quickly and easily show EVE Online ship fits in your own application", + "scripts": { + "build": "rollup -c", + "build-storybook": "storybook build", + "dev": "storybook dev -p 6006 --no-open", + "lint": "eslint src --ext .js,.tsx --cache" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/EVEShipFit/react.git" + }, + "author": "Patric Stout ", + "license": "MIT", + "dependencies": { + "@eveshipfit/dogma-engine": "^1.1.0", + "clsx": "^2.0.0" + }, + "devDependencies": { + "@babel/preset-env": "^7.23.3", + "@babel/preset-react": "^7.23.3", + "@babel/preset-typescript": "^7.23.3", + "@rollup/plugin-commonjs": "^25.0.7", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", + "@storybook/addon-essentials": "^7.5.3", + "@storybook/addon-interactions": "^7.5.3", + "@storybook/addon-links": "^7.5.3", + "@storybook/blocks": "^7.5.1", + "@storybook/react": "^7.5.1", + "@storybook/react-webpack5": "^7.5.3", + "@storybook/testing-library": "^0.2.2", + "@types/react": "^18.2.37", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", + "eslint": "^8.53.0", + "eslint-config-airbnb": "19.0.4", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-import": "^2.29.0", + "eslint-plugin-jsx-a11y": "^6.8.0", + "eslint-plugin-prettier": "^5.0.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-storybook": "^0.6.15", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup": "^4.4.0", + "rollup-plugin-dts": "^6.1.0", + "rollup-plugin-esbuild": "^6.1.0", + "rollup-plugin-node-externals": "^6.1.2", + "rollup-plugin-postcss": "^4.0.2", + "storybook": "^7.5.3", + "tslib": "^2.6.2", + "typescript": "^5.2.2", + "typescript-plugin-css-modules": "^5.0.2" + }, + "peerDependencies": { + "react": "^18.2.0" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "files": [ + "dist" + ], + "types": "dist/index.d.ts" +} diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..9323885 --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,45 @@ +import commonjs from "@rollup/plugin-commonjs"; +import dts from "rollup-plugin-dts"; +import esbuild from "rollup-plugin-esbuild"; +import nodeExternals from "rollup-plugin-node-externals"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import postcss from "rollup-plugin-postcss"; +import terser from "@rollup/plugin-terser"; + +export default [ + { + input: "src/index.ts", + output: [ + { + file: "dist/cjs/index.js", + format: "cjs", + sourcemap: true, + }, + { + file: "dist/esm/index.js", + format: "esm", + sourcemap: true, + }, + ], + plugins: [ + nodeExternals(), + nodeResolve(), + commonjs(), + esbuild({ tsconfig: "./tsconfig.json" }), + postcss({ modules: true }), + terser(), + ], + }, + { + input: "src/index.ts", + output: [ + { + file: "dist/index.d.ts", + format: "esm", + }, + ], + plugins: [ + dts({ tsconfig: "./tsconfig.json" }), + ], + }, +]; diff --git a/src/DogmaEngineProvider/DogmaEngineProvider.stories.tsx b/src/DogmaEngineProvider/DogmaEngineProvider.stories.tsx new file mode 100644 index 0000000..57ca678 --- /dev/null +++ b/src/DogmaEngineProvider/DogmaEngineProvider.stories.tsx @@ -0,0 +1,56 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EveDataProvider } from '../EveDataProvider'; +import { DogmaEngineContext, DogmaEngineProvider } from './'; + +const meta: Meta = { + component: DogmaEngineProvider, + tags: ['autodocs'], + title: 'Provider/DogmaEngineProvider', +}; + +export default meta; +type Story = StoryObj; + +/** Convert an ES6 map to an Object, which JSON can stringify. */ +function MapToDict(_key: string, value: unknown) { + if (value instanceof Map) { + return Array.from(value.entries()).reduce((obj, [key, item]) => ( + Object.assign(obj, { [key]: item }) + ), {}); + } + + return value; +} + +const TestDogmaEngine = () => { + const dogmaEngine = React.useContext(DogmaEngineContext); + + if (dogmaEngine?.loaded) { + const stats = dogmaEngine.engine?.calculate({hull: 12747, items: [20639]}, {}); + + return ( +
+ DogmaEngine: loaded
+ Stats: {JSON.stringify(stats, MapToDict)} +
+ ) + } + + return ( +
+ DogmaEngine: loading
+
+ ); +} + +export const Default: Story = { + render: () => ( + + + + + + ), +}; diff --git a/src/DogmaEngineProvider/DogmaEngineProvider.tsx b/src/DogmaEngineProvider/DogmaEngineProvider.tsx new file mode 100644 index 0000000..e92653c --- /dev/null +++ b/src/DogmaEngineProvider/DogmaEngineProvider.tsx @@ -0,0 +1,104 @@ +import React from "react"; + +import { DogmaAttribute, DogmaEffect, TypeDogmaAttribute, TypeDogmaEffect, TypeID, EveDataContext } from "../EveDataProvider"; +import type { init, calculate } from "@eveshipfit/dogma-engine"; + +interface EsfDogmaEngine { + init: typeof init, + calculate: typeof calculate, +} + +interface DogmaEngine { + loaded?: boolean, + loadedData?: boolean, + engine?: EsfDogmaEngine, +} + +export const DogmaEngineContext = React.createContext({}); + +declare global { + interface Window { + get_dogma_attributes?: unknown; + get_dogma_attribute?: unknown; + get_dogma_effects?: unknown; + get_dogma_effect?: unknown; + get_type_id?: unknown; + } +} + +export interface DogmaEngineProps { + /** Children that can use this provider. */ + children: React.ReactNode; +} + +/** + * Provides method of calculating accurate attributes for a ship fit. + * + * ```typescript + * const dogmaEngine = React.useContext(DogmaEngineContext); + * + * if (dogmaEngine?.loaded) { + * // calculate({hull: number, items: number[]}, skills: Map) + * const stats = dogmaEngine.engine.calculate({hull: 12747, items: [20639]}, []); + * console.log(stats); + * } + * ``` + */ +export const DogmaEngineProvider = (props: DogmaEngineProps) => { + const [dogmaEngine, setDogmaEngine] = React.useState({}); + const eveData = React.useContext(EveDataContext); + + React.useEffect(() => { + if (!eveData.loaded) return; + + setDogmaEngine((prevDogmaEngine: DogmaEngine) => { + return { + ...prevDogmaEngine, + loadedData: true, + loaded: prevDogmaEngine.engine !== undefined, + }; + }); + + window.get_dogma_attributes = (type_id: number): TypeDogmaAttribute[] | undefined => { + return eveData.typeDogma?.[type_id].dogmaAttributes; + }; + window.get_dogma_attribute = (attribute_id: number): DogmaAttribute | undefined => { + return eveData.dogmaAttributes?.[attribute_id]; + }; + window.get_dogma_effects = (type_id: number): TypeDogmaEffect[] | undefined => { + return eveData.typeDogma?.[type_id].dogmaEffects; + }; + window.get_dogma_effect = (effect_id: number): DogmaEffect | undefined => { + return eveData.dogmaEffects?.[effect_id]; + }; + window.get_type_id = (type_id: number): TypeID | undefined => { + return eveData.typeIDs?.[type_id]; + }; + + return () => { + window.get_dogma_attributes = undefined; + window.get_dogma_attribute = undefined; + window.get_dogma_effects = undefined; + window.get_dogma_effect = undefined; + window.get_type_id = undefined; + } + }, [eveData]); + + React.useEffect(() => { + import("@eveshipfit/dogma-engine").then((newDogmaEngine) => { + newDogmaEngine.init(); + + setDogmaEngine((prevDogmaEngine: DogmaEngine) => { + return { + ...prevDogmaEngine, + engine: newDogmaEngine, + loaded: prevDogmaEngine.loadedData, + }; + }); + }); + }, []); + + return + {props.children} + +}; diff --git a/src/DogmaEngineProvider/index.ts b/src/DogmaEngineProvider/index.ts new file mode 100644 index 0000000..6a74d6e --- /dev/null +++ b/src/DogmaEngineProvider/index.ts @@ -0,0 +1 @@ +export { DogmaEngineContext, DogmaEngineProvider } from "./DogmaEngineProvider"; diff --git a/src/EveDataProvider/DataTypes.tsx b/src/EveDataProvider/DataTypes.tsx new file mode 100644 index 0000000..db693d8 --- /dev/null +++ b/src/EveDataProvider/DataTypes.tsx @@ -0,0 +1,62 @@ + +export interface TypeDogmaAttribute { + attributeID: number, + value: number, +} + +export interface TypeDogmaEffect { + effectID: number, + isDefault: boolean, +} + +export interface TypeDogma { + dogmaAttributes: TypeDogmaAttribute[], + dogmaEffects: TypeDogmaEffect[], +} + +export interface TypeID { + name: string, + groupID: number, + categoryID: number, + published: boolean, + marketGroupID?: number, + capacity?: number, + mass?: number, + radius?: number, + volume?: number, +} + +export interface DogmaAttribute { + name: string + published: boolean, + defaultValue: number, + highIsGood: boolean, + stackable: boolean, +} + +export interface DogmaEffect { + name: string, + effectCategory: number, + electronicChance: boolean, + isAssistance: boolean, + isOffensive: boolean, + isWarpSafe: boolean, + propulsionChance: boolean, + rangeChance: boolean, + dischargeAttributeID?: number, + durationAttributeID?: number, + rangeAttributeID?: number, + falloffAttributeID?: number, + trackingSpeedAttributeID?: number, + fittingUsageChanceAttributeID?: number, + resistanceAttributeID?: number, + modifierInfo: { + domain: number, + func: number, + modifiedAttributeID?: number, + modifyingAttributeID?: number, + operation?: number, + groupID?: number, + skillTypeID?: number, + }[], +} diff --git a/src/EveDataProvider/EveDataProvider.stories.tsx b/src/EveDataProvider/EveDataProvider.stories.tsx new file mode 100644 index 0000000..431d461 --- /dev/null +++ b/src/EveDataProvider/EveDataProvider.stories.tsx @@ -0,0 +1,37 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EveDataContext, EveDataProvider } from './'; + +const meta: Meta = { + component: EveDataProvider, + tags: ['autodocs'], + title: 'Provider/EveDataProvider', +}; + +export default meta; +type Story = StoryObj; + +const TestEveData = () => { + const eveData = React.useContext(EveDataContext); + + return ( +
+ TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}
+ TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}
+ DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}
+ DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}
+ AttributeMapper: {eveData.attributeMapping ? Object.keys(eveData.attributeMapping).length : "loading"}
+
+ All loaded: {eveData.loaded ? "yes" : "no"} +
+ ); +} + +export const Default: Story = { + render: () => ( + + + + ), +}; diff --git a/src/EveDataProvider/EveDataProvider.tsx b/src/EveDataProvider/EveDataProvider.tsx new file mode 100644 index 0000000..b6b3681 --- /dev/null +++ b/src/EveDataProvider/EveDataProvider.tsx @@ -0,0 +1,111 @@ +import React from "react"; + +import { DogmaAttribute, DogmaEffect, TypeDogma, TypeID } from "./DataTypes"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +// eslint-disable-next-line import/extensions +import * as esf_pb2 from "./esf_pb2.js"; + +const defaultDataUrl = "https://data.eveship.fit/20231023/"; + +interface DogmaData { + loaded?: boolean; + typeIDs?: Record; + typeDogma?: Record; + dogmaEffects?: Record; + dogmaAttributes?: Record; + attributeMapping?: Record; +} + +export const EveDataContext = React.createContext({}); + +export interface DogmaDataProps { + dataUrl?: string; + + /** Children that can use this provider. */ + children: React.ReactNode; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +async function fetchDataFile(dataUrl: string, name: string, pb2: any): Promise { + const response = await fetch(dataUrl + name + ".pb2"); + const contentLength = response.headers.get("content-length"); + const reader = response.body?.getReader(); + const result = await pb2.decode(reader, contentLength); + + return result.entries; +} + +function isLoaded(dogmaData: DogmaData): boolean | undefined { + if (dogmaData.typeIDs === undefined) return undefined; + if (dogmaData.typeDogma === undefined) return undefined; + if (dogmaData.dogmaEffects === undefined) return undefined; + if (dogmaData.dogmaAttributes === undefined) return undefined; + if (dogmaData.attributeMapping === undefined) return undefined; + + return true; +} + +/** + * Provides information like TypeIDs, Dogma information, etc. + * + * ```typescript + * const eveData = React.useContext(EveDataContext); + * + * if (eveData?.loaded) { + * console.log(eveData.typeIDs.length); + * } + * ``` + */ +export const EveDataProvider = (props: DogmaDataProps) => { + const dataUrl = props.dataUrl ?? defaultDataUrl; + const [dogmaData, setDogmaData] = React.useState({}); + + React.useEffect(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + function fetchAndLoadDataFile(name: string, pb2: any) { + fetchDataFile(dataUrl, name, pb2).then((result) => { + setDogmaData((prevDogmaData: DogmaData) => { + const newDogmaData = { + ...prevDogmaData, + [name]: result, + }; + + newDogmaData.loaded = isLoaded(newDogmaData); + return newDogmaData; + } + )}); + } + + fetchAndLoadDataFile("typeIDs", esf_pb2.esf.TypeIDs); + fetchAndLoadDataFile("typeDogma", esf_pb2.esf.TypeDogma); + fetchAndLoadDataFile("dogmaEffects", esf_pb2.esf.DogmaEffects); + fetchAndLoadDataFile("dogmaAttributes", esf_pb2.esf.DogmaAttributes); + }, [dataUrl]); + + React.useEffect(() => { + if (!dogmaData.dogmaAttributes) return; + + /* Create a reverse mapping to quickly lookup attribute name to attribute ID. */ + const attributeMapping: Record = {}; + for (const id in dogmaData.dogmaAttributes) { + const name = dogmaData.dogmaAttributes[id].name; + attributeMapping[name] = parseInt(id); + } + + setDogmaData((prevDogmaData: DogmaData) => { + const newDogmaData = { + ...prevDogmaData, + attributeMapping: attributeMapping, + } + + newDogmaData.loaded = isLoaded(newDogmaData); + return newDogmaData; + }); + }, [dogmaData.dogmaAttributes]); + + return + {props.children} + +}; diff --git a/src/EveDataProvider/esf_pb2.js b/src/EveDataProvider/esf_pb2.js new file mode 100644 index 0000000..71369b1 --- /dev/null +++ b/src/EveDataProvider/esf_pb2.js @@ -0,0 +1,732 @@ +/*eslint-disable block-scoped-var, id-length, no-control-regex, no-magic-numbers, no-prototype-builtins, no-redeclare, no-shadow, no-var, sort-vars*/ +import $Reader from "./protobuf.js"; + +const $root = {}; +const emptyArray = Object.freeze ? Object.freeze([]) : []; +const emptyObject = Object.freeze ? Object.freeze({}) : {}; + +export const esf = $root.esf = (() => { + + const esf = {}; + + esf.TypeDogma = (function() { + + function TypeDogma(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]]; + } + + TypeDogma.prototype.entries = emptyObject; + + TypeDogma.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.TypeDogma(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + + 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.TypeDogma.TypeDogmaEntry.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + TypeDogma.TypeDogmaEntry = (function() { + + function TypeDogmaEntry(p) { + this.dogmaAttributes = []; + this.dogmaEffects = []; + 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]]; + } + + TypeDogmaEntry.prototype.dogmaAttributes = emptyArray; + TypeDogmaEntry.prototype.dogmaEffects = emptyArray; + + TypeDogmaEntry.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.TypeDogma.TypeDogmaEntry(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + if (!(m.dogmaAttributes && m.dogmaAttributes.length)) + m.dogmaAttributes = []; + m.dogmaAttributes.push($root.esf.TypeDogma.TypeDogmaEntry.DogmaAttributes.decode(r, r.uint32())); + break; + } + case 2: { + if (!(m.dogmaEffects && m.dogmaEffects.length)) + m.dogmaEffects = []; + m.dogmaEffects.push($root.esf.TypeDogma.TypeDogmaEntry.DogmaEffects.decode(r, r.uint32())); + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + TypeDogmaEntry.DogmaAttributes = (function() { + + function DogmaAttributes(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]]; + } + + DogmaAttributes.prototype.attributeID = 0; + DogmaAttributes.prototype.value = 0; + + DogmaAttributes.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.TypeDogma.TypeDogmaEntry.DogmaAttributes(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.attributeID = r.int32(); + break; + } + case 2: { + m.value = r.float(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("attributeID")) + throw Error("missing required 'attributeID'", { instance: m }); + if (!m.hasOwnProperty("value")) + throw Error("missing required 'value'", { instance: m }); + return m; + }; + + return DogmaAttributes; + })(); + + TypeDogmaEntry.DogmaEffects = (function() { + + function DogmaEffects(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]]; + } + + DogmaEffects.prototype.effectID = 0; + DogmaEffects.prototype.isDefault = false; + + DogmaEffects.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.TypeDogma.TypeDogmaEntry.DogmaEffects(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.effectID = r.int32(); + break; + } + case 2: { + m.isDefault = r.bool(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("effectID")) + throw Error("missing required 'effectID'", { instance: m }); + if (!m.hasOwnProperty("isDefault")) + throw Error("missing required 'isDefault'", { instance: m }); + return m; + }; + + return DogmaEffects; + })(); + + return TypeDogmaEntry; + })(); + + return TypeDogma; + })(); + + esf.TypeIDs = (function() { + + function TypeIDs(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]]; + } + + TypeIDs.prototype.entries = emptyObject; + + TypeIDs.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.TypeIDs(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + + 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.TypeIDs.TypeID.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + TypeIDs.TypeID = (function() { + + function TypeID(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]]; + } + + TypeID.prototype.name = ""; + TypeID.prototype.groupID = 0; + TypeID.prototype.categoryID = 0; + TypeID.prototype.published = false; + TypeID.prototype.marketGroupID = 0; + TypeID.prototype.capacity = 0; + TypeID.prototype.mass = 0; + TypeID.prototype.radius = 0; + TypeID.prototype.volume = 0; + + TypeID.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.TypeIDs.TypeID(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.name = r.string(); + break; + } + case 2: { + m.groupID = r.int32(); + break; + } + case 3: { + m.categoryID = r.int32(); + break; + } + case 4: { + m.published = r.bool(); + break; + } + case 5: { + m.marketGroupID = r.int32(); + break; + } + case 6: { + m.capacity = r.float(); + break; + } + case 7: { + m.mass = r.float(); + break; + } + case 8: { + m.radius = r.float(); + break; + } + case 9: { + m.volume = r.float(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("name")) + throw Error("missing required 'name'", { instance: m }); + if (!m.hasOwnProperty("groupID")) + throw Error("missing required 'groupID'", { 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 TypeID; + })(); + + return TypeIDs; + })(); + + esf.DogmaAttributes = (function() { + + function DogmaAttributes(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]]; + } + + DogmaAttributes.prototype.entries = emptyObject; + + DogmaAttributes.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.DogmaAttributes(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + + 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.DogmaAttributes.DogmaAttribute.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + DogmaAttributes.DogmaAttribute = (function() { + + function DogmaAttribute(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]]; + } + + DogmaAttribute.prototype.name = ""; + DogmaAttribute.prototype.published = false; + DogmaAttribute.prototype.defaultValue = 0; + DogmaAttribute.prototype.highIsGood = false; + DogmaAttribute.prototype.stackable = false; + + DogmaAttribute.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.DogmaAttributes.DogmaAttribute(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.name = r.string(); + break; + } + case 2: { + m.published = r.bool(); + break; + } + case 3: { + m.defaultValue = r.float(); + break; + } + case 4: { + m.highIsGood = r.bool(); + break; + } + case 5: { + m.stackable = r.bool(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("name")) + throw Error("missing required 'name'", { instance: m }); + if (!m.hasOwnProperty("published")) + throw Error("missing required 'published'", { instance: m }); + if (!m.hasOwnProperty("defaultValue")) + throw Error("missing required 'defaultValue'", { instance: m }); + if (!m.hasOwnProperty("highIsGood")) + throw Error("missing required 'highIsGood'", { instance: m }); + if (!m.hasOwnProperty("stackable")) + throw Error("missing required 'stackable'", { instance: m }); + return m; + }; + + return DogmaAttribute; + })(); + + return DogmaAttributes; + })(); + + esf.DogmaEffects = (function() { + + function DogmaEffects(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]]; + } + + DogmaEffects.prototype.entries = emptyObject; + + DogmaEffects.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.DogmaEffects(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + + 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.DogmaEffects.DogmaEffect.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + DogmaEffects.DogmaEffect = (function() { + + function DogmaEffect(p) { + this.modifierInfo = []; + 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]]; + } + + DogmaEffect.prototype.name = ""; + DogmaEffect.prototype.effectCategory = 0; + DogmaEffect.prototype.electronicChance = false; + DogmaEffect.prototype.isAssistance = false; + DogmaEffect.prototype.isOffensive = false; + 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.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.DogmaEffects.DogmaEffect(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.name = r.string(); + break; + } + case 2: { + m.effectCategory = r.int32(); + break; + } + case 3: { + m.electronicChance = r.bool(); + break; + } + case 4: { + m.isAssistance = r.bool(); + break; + } + case 5: { + m.isOffensive = r.bool(); + break; + } + case 6: { + m.isWarpSafe = r.bool(); + break; + } + case 7: { + m.propulsionChance = r.bool(); + break; + } + case 8: { + m.rangeChance = r.bool(); + break; + } + case 9: { + m.dischargeAttributeID = r.int32(); + break; + } + case 10: { + m.durationAttributeID = r.int32(); + break; + } + case 11: { + m.rangeAttributeID = r.int32(); + break; + } + case 12: { + m.falloffAttributeID = r.int32(); + break; + } + case 13: { + m.trackingSpeedAttributeID = r.int32(); + break; + } + case 14: { + m.fittingUsageChanceAttributeID = r.int32(); + break; + } + case 15: { + m.resistanceAttributeID = r.int32(); + break; + } + case 16: { + if (!(m.modifierInfo && m.modifierInfo.length)) + m.modifierInfo = []; + m.modifierInfo.push($root.esf.DogmaEffects.DogmaEffect.ModifierInfo.decode(r, r.uint32())); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("name")) + throw Error("missing required 'name'", { instance: m }); + if (!m.hasOwnProperty("effectCategory")) + throw Error("missing required 'effectCategory'", { instance: m }); + if (!m.hasOwnProperty("electronicChance")) + throw Error("missing required 'electronicChance'", { instance: m }); + if (!m.hasOwnProperty("isAssistance")) + throw Error("missing required 'isAssistance'", { instance: m }); + if (!m.hasOwnProperty("isOffensive")) + throw Error("missing required 'isOffensive'", { instance: m }); + if (!m.hasOwnProperty("isWarpSafe")) + throw Error("missing required 'isWarpSafe'", { instance: m }); + if (!m.hasOwnProperty("propulsionChance")) + throw Error("missing required 'propulsionChance'", { instance: m }); + if (!m.hasOwnProperty("rangeChance")) + throw Error("missing required 'rangeChance'", { instance: m }); + return m; + }; + + DogmaEffect.ModifierInfo = (function() { + + function ModifierInfo(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]]; + } + + 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.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.DogmaEffects.DogmaEffect.ModifierInfo(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.domain = r.int32(); + break; + } + case 2: { + m.func = r.int32(); + break; + } + case 3: { + m.modifiedAttributeID = r.int32(); + break; + } + case 4: { + m.modifyingAttributeID = r.int32(); + break; + } + case 5: { + m.operation = r.int32(); + break; + } + case 6: { + m.groupID = r.int32(); + break; + } + case 7: { + m.skillTypeID = r.int32(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("domain")) + throw Error("missing required 'domain'", { instance: m }); + if (!m.hasOwnProperty("func")) + throw Error("missing required 'func'", { instance: m }); + return m; + }; + + ModifierInfo.Domain = (function() { + const valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "itemID"] = 0; + values[valuesById[1] = "shipID"] = 1; + values[valuesById[2] = "charID"] = 2; + values[valuesById[3] = "otherID"] = 3; + values[valuesById[4] = "structureID"] = 4; + values[valuesById[5] = "target"] = 5; + values[valuesById[6] = "targetID"] = 6; + return values; + })(); + + ModifierInfo.Func = (function() { + const valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "ItemModifier"] = 0; + values[valuesById[1] = "LocationGroupModifier"] = 1; + values[valuesById[2] = "LocationModifier"] = 2; + values[valuesById[3] = "LocationRequiredSkillModifier"] = 3; + values[valuesById[4] = "OwnerRequiredSkillModifier"] = 4; + values[valuesById[5] = "EffectStopper"] = 5; + return values; + })(); + + return ModifierInfo; + })(); + + return DogmaEffect; + })(); + + return DogmaEffects; + })(); + + return esf; +})(); + +export { $root as default }; diff --git a/src/EveDataProvider/index.ts b/src/EveDataProvider/index.ts new file mode 100644 index 0000000..dd6761c --- /dev/null +++ b/src/EveDataProvider/index.ts @@ -0,0 +1,2 @@ +export { EveDataContext, EveDataProvider } from "./EveDataProvider"; +export type { DogmaAttribute, DogmaEffect, TypeDogmaAttribute, TypeDogmaEffect, TypeID } from "./DataTypes"; diff --git a/src/EveDataProvider/protobuf.js b/src/EveDataProvider/protobuf.js new file mode 100644 index 0000000..02cce18 --- /dev/null +++ b/src/EveDataProvider/protobuf.js @@ -0,0 +1,128 @@ +"use strict"; + +/* + * A stripped down copy of protobuf.js's reader. + * + * The minimal version of protobuf.js is 100KB. But most of the stuff is not used. + * Additionally, it has no way to read from a stream. This version addresses both: + * only implement that what we actually need, and read from a stream when needed. + * + * The smaller size means the javascript loads faster, and the streaming means the + * decoding can start on the first chunk (instead of when all data arrived). + * + * NOTE: this is not a generic Protobuf loader, and when you feed it broken protobuf + * files, it will crash in unexpected ways. + * + * Original source: https://github.com/protobufjs/protobuf.js/blob/master/src/reader.js + */ + +module.exports = Reader; + +function Reader(reader) { + this._reader = reader; + this._buf = new Uint8Array(); + this._next_buf = null; + this._buf_pos = 0; + + this.pos = 0; + this.len = 0; +} + +Reader.create = function create(reader) { + return new Reader(reader); +} + +Reader.prototype.need_data = function need_data() { + return this._buf.length - this._buf_pos < 2048 && this._next_buf === null; +} + +Reader.prototype.fetch_data = async function fetch_data() { + if (this._buf.length - this._buf_pos >= 2048) return; + if (this._next_buf !== null) return; + + const {done, value} = await this._reader.read(); + if (done) { + this._next_buf = new Uint8Array(); + } else { + this._next_buf = value; + } +} + +Reader.prototype.read = function read(len) { + if (this._next_buf === null) return this._buf; + + if (this._buf_pos > this._buf.length) { + this._buf_pos -= this._buf.length; + this._buf = this._next_buf; + + this._next_buf = null; + } else if (this._buf_pos + len > this._buf.length) { + this._buf = this._buf.slice(this._buf_pos); + this._buf_pos = 0; + return new Uint8Array([...this._buf, ...this._next_buf.slice(0, len - this._buf.length)]); + } + + return this._buf; +} + +Reader.prototype.uint32 = (function read_uint32_setup() { + var value = 4294967295; // optimizer type-hint, tends to deopt otherwise (?!) + return function read_uint32() { + const buf = this.read(10); + + value = ( buf[this._buf_pos] & 127 ) >>> 0; this.pos++; if (buf[this._buf_pos++] < 128) return value; + value = (value | (buf[this._buf_pos] & 127) << 7) >>> 0; this.pos++; if (buf[this._buf_pos++] < 128) return value; + value = (value | (buf[this._buf_pos] & 127) << 14) >>> 0; this.pos++; if (buf[this._buf_pos++] < 128) return value; + value = (value | (buf[this._buf_pos] & 127) << 21) >>> 0; this.pos++; if (buf[this._buf_pos++] < 128) return value; + value = (value | (buf[this._buf_pos] & 15) << 28) >>> 0; this.pos++; if (buf[this._buf_pos++] < 128) return value; + + this.pos += 5; + this._buf_pos += 5; + return value; + }; +})(); + +Reader.prototype.int32 = function read_int32() { + return this.uint32() | 0; +}; + +Reader.prototype.bool = function read_bool() { + return this.uint32() !== 0; +} + +Reader.prototype.bytes = function read_bytes() { + var length = this.uint32(); + + const buf = this.read(length); + this.pos += length; + + const res = buf.slice(this._buf_pos, this._buf_pos + length); + this._buf_pos += length; + return res; +}; + +Reader.prototype.string = function read_string() { + var bytes = this.bytes(); + var str = ""; + for (var i = 0; i < bytes.length; i++) { + str += String.fromCharCode(bytes[i]); + } + return str; +}; + +let f32 = new Float32Array([ -0 ]), f8b = new Uint8Array(f32.buffer); + +Reader.prototype.float = function read_float() { + const buf = this.read(4); + this.pos += 4; + + f8b[0] = buf[this._buf_pos++]; + f8b[1] = buf[this._buf_pos++]; + f8b[2] = buf[this._buf_pos++]; + f8b[3] = buf[this._buf_pos++]; + return f32[0]; +}; + +Reader.prototype.skipType = function(wireType) { + throw Error("Please avoid skipping fields; it is really slow."); +}; diff --git a/src/FormatEftToEsi/FormatEftToEsi.stories.tsx b/src/FormatEftToEsi/FormatEftToEsi.stories.tsx new file mode 100644 index 0000000..fe29766 --- /dev/null +++ b/src/FormatEftToEsi/FormatEftToEsi.stories.tsx @@ -0,0 +1,57 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EveDataProvider } from '../EveDataProvider'; +import { FormatEftToEsi } from './FormatEftToEsi'; + +const meta: Meta = { + component: FormatEftToEsi, + tags: ['autodocs'], + title: 'Function/EftToEsiJson', +}; + +const withEveDataProvider: Decorator<{eft: string}> = (Story) => { + return ( + + + + ); +} + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + eft: `[Loki,Loki basic PVE] +Caldari Navy Ballistic Control System +Caldari Navy Ballistic Control System +Caldari Navy Ballistic Control System +Damage Control II + +Gist X-Type Large Shield Booster +Republic Fleet Large Cap Battery +Missile Guidance Computer II +10MN Afterburner II +Multispectrum Shield Hardener II + +Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile +Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile +Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile +Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile +Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile +Covert Ops Cloaking Device II +Sisters Core Probe Launcher + +Medium Hydraulic Bay Thrusters II +Medium Rocket Fuel Cache Partition II +Medium Rocket Fuel Cache Partition I + +Loki Core - Augmented Nuclear Reactor +Loki Defensive - Covert Reconfiguration +Loki Offensive - Launcher Efficiency Configuration +Loki Propulsion - Wake Limiter +`, + }, + decorators: [withEveDataProvider], +}; diff --git a/src/FormatEftToEsi/FormatEftToEsi.tsx b/src/FormatEftToEsi/FormatEftToEsi.tsx new file mode 100644 index 0000000..585d359 --- /dev/null +++ b/src/FormatEftToEsi/FormatEftToEsi.tsx @@ -0,0 +1,122 @@ +import React from "react"; + +import { EveDataContext } from '../EveDataProvider'; +import { EsiFit } from "../ShipSnapshotProvider"; + +/** Mapping between slot types and ESI flags (for first slot in the type). */ +const EsiFlagMapping: Record = { + "low": 11, + "med": 19, + "high": 27, + "rig": 92, + "sub": 125, +}; + +/** Mapping between dogma effect IDs and slot types. */ +const EffectIdMapping: Record = { + 11: "low", + 13: "med", + 12: "high", + 2663: "rig", + 3772: "sub", +}; + +/** + * Convert an EFT string to an ESI JSON object. + */ +export function useFormatEftToEsi() { + const eveData = React.useContext(EveDataContext); + + return (eft: string): EsiFit | undefined => { + if (!eveData?.loaded) return undefined; + + function lookupTypeByName(name: string): number | undefined { + for (const typeId in eveData.typeIDs) { + const type = eveData.typeIDs[typeId]; + + if (type.name === name) { + return parseInt(typeId); + } + } + + return undefined; + } + + const esiFit: EsiFit = { + name: "EFT Import", + description: "", + ship_type_id: 0, + items: [], + }; + + const lines = eft.trim().split("\n"); + + if (!lines[0].startsWith("[")) return undefined; + if (!lines[0].endsWith("]")) return undefined; + + const shipType = lines[0].split(",")[0].slice(1); + const shipTypeId = lookupTypeByName(shipType); + if (shipTypeId === undefined) throw new Error(`Unknown ship '${shipType}'.`); + + esiFit.ship_type_id = shipTypeId; + esiFit.name = lines[0].split(",")[1].slice(0, -1).trim(); + + const slotIndex: Record = { + "low": 0, + "med": 0, + "high": 0, + "rig": 0, + "sub": 0, + }; + + let lastSlotType = ""; + for (let i = 1; i < lines.length; i++) { + const line = lines[i]; + if (line.trim() === "") continue; + + if (line.startsWith("[Empty ")) { + if (lastSlotType != "") { + slotIndex[lastSlotType]++; + } + continue; + } + + const itemType = line.split(",")[0].trim(); + const itemTypeId = lookupTypeByName(itemType); + if (itemTypeId === undefined) throw new Error(`Unknown item '${itemType}'.`); + + const effects = eveData.typeDogma?.[itemTypeId]?.dogmaEffects; + if (effects === undefined) throw new Error(`No dogma defined for item '${itemType}'.`); + + /* Find what type of slot this item goes into. */ + let slotType = ""; + for (const effectId in effects) { + slotType = EffectIdMapping[effects[effectId].effectID]; + if (slotType) break; + } + lastSlotType = slotType; + + /* Ignore items we don't care about. */ + if (slotType === "") continue; + + esiFit.items.push({"flag": EsiFlagMapping[slotType] + slotIndex[slotType], "quantity": 1, "type_id": itemTypeId}); + slotIndex[slotType]++; + } + + return esiFit; + }; +}; + +export interface FormatEftToEsiProps { + /** The EFT string. */ + eft: string; +} + +/** + * Use useFormatEftToEsi() instead of this component. + */ +export const FormatEftToEsi = (props: FormatEftToEsiProps) => { + const esiFit = useFormatEftToEsi(); + + return
{JSON.stringify(esiFit(props.eft), null, 2)}
+}; diff --git a/src/FormatEftToEsi/index.ts b/src/FormatEftToEsi/index.ts new file mode 100644 index 0000000..f831442 --- /dev/null +++ b/src/FormatEftToEsi/index.ts @@ -0,0 +1 @@ +export { useFormatEftToEsi } from "./FormatEftToEsi"; diff --git a/src/ShipAttribute/ShipAttribute.stories.tsx b/src/ShipAttribute/ShipAttribute.stories.tsx new file mode 100644 index 0000000..a58d4cb --- /dev/null +++ b/src/ShipAttribute/ShipAttribute.stories.tsx @@ -0,0 +1,41 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { ShipAttribute } from './'; + +const meta: Meta = { + component: ShipAttribute, + tags: ['autodocs'], + title: 'Component/ShipAttribute', +}; + +export default meta; +type Story = StoryObj; + +const withShipSnapshotProvider: Decorator<{name: string}> = (Story, context) => { + return ( + + + + + + + + ); +} + +export const Default: Story = { + args: { + name: "cpuUsage", + }, + decorators: [withShipSnapshotProvider], + parameters: { + snapshot: { + fit: JSON.parse("{\"name\": \"test\", \"ship_type_id\": 12747, \"items\": [{\"flag\": 11, \"quantity\": 1, \"type_id\": 20639}]}"), + skills: {}, + } + }, +}; diff --git a/src/ShipAttribute/ShipAttribute.tsx b/src/ShipAttribute/ShipAttribute.tsx new file mode 100644 index 0000000..764f4e9 --- /dev/null +++ b/src/ShipAttribute/ShipAttribute.tsx @@ -0,0 +1,72 @@ +import React from "react"; + +import { EveDataContext } from '../EveDataProvider'; +import { ShipSnapshotContext } from '../ShipSnapshotProvider'; + +export interface ShipAttributeProps { + /** Name of the attribute. */ + name: string; + /** How many decimals to render. */ + fixed: number; + /** Whether this is a resistance attribute. */ + isResistance?: boolean; + /** With what value, if any, to divide the attribute value. */ + divideBy?: number; +} + +/** + * Return the value of a ship's attribute. + */ +export function useShipAttribute(props: ShipAttributeProps) { + const eveData = React.useContext(EveDataContext); + const shipSnapshot = React.useContext(ShipSnapshotContext); + + if (shipSnapshot?.loaded) { + const attributeId = eveData.attributeMapping?.[props.name] || 0; + let value = shipSnapshot.hull?.attributes.get(attributeId)?.value; + let highIsGood = eveData.dogmaAttributes?.[attributeId]?.highIsGood; + + if (value == undefined) { + return "?"; + } + + if (props.isResistance) { + value = 100 - value * 100; + highIsGood = !highIsGood; + } + + if (props.divideBy) { + value /= props.divideBy; + } + + const k = Math.pow(10, props.fixed); + if (k > 0) { + if (highIsGood) { + value -= 1 / k / 10; + value = Math.ceil(value * k) / k; + } else { + value += 1 / k / 10; + value = Math.floor(value * k) / k; + } + } + + /* Make sure we don't display "-0", but "0" instead. */ + if (Object.is(value, -0)) { + value = 0; + } + + return value.toLocaleString("en", { + minimumFractionDigits: props.fixed, + maximumFractionDigits: props.fixed, + }); + } +}; + +/** + * Render a single attribute of a ship's snapshot. + */ +export const ShipAttribute = (props: ShipAttributeProps) => { + const stringValue = useShipAttribute(props); + + return {stringValue} +}; diff --git a/src/ShipAttribute/index.ts b/src/ShipAttribute/index.ts new file mode 100644 index 0000000..620ab71 --- /dev/null +++ b/src/ShipAttribute/index.ts @@ -0,0 +1 @@ +export { useShipAttribute, ShipAttribute } from "./ShipAttribute"; diff --git a/src/ShipFit/Hull.tsx b/src/ShipFit/Hull.tsx new file mode 100644 index 0000000..6c591bb --- /dev/null +++ b/src/ShipFit/Hull.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import { ShipSnapshotContext } from '../ShipSnapshotProvider'; + +import styles from "./ShipFit.module.css"; + +export interface ShipFitProps { + radius: number; +} + +export const Hull = () => { + const shipSnapshot = React.useContext(ShipSnapshotContext); + + const hull = shipSnapshot?.fit?.ship_type_id; + if (hull === undefined) { + return <> + } + + return
+
+ +
+
+} diff --git a/src/ShipFit/ShipFit.module.css b/src/ShipFit/ShipFit.module.css new file mode 100644 index 0000000..7f5ff47 --- /dev/null +++ b/src/ShipFit/ShipFit.module.css @@ -0,0 +1,90 @@ + +.fit { + border-radius: 50%; + border: 1px solid black; + height: calc(var(--radius) * 2); + position: relative; + width: calc(var(--radius) * 2); + + --radius-slots: calc(var(--radius) * 0.92); +} + +.outerBand { + border-radius: 50%; + border: calc((var(--radius) - var(--radius-slots)) * 0.8) solid black; + box-sizing: border-box; + height: calc(var(--radius) * 2); + position: absolute; + width: calc(var(--radius) * 2); + z-index: 2; +} + +.innerBand { + border-radius: 50%; + border: calc(var(--radius-slots) / 6 + var(--radius) - var(--radius-slots)) solid black; + box-sizing: border-box; + height: calc(var(--radius) * 2); + opacity: 0.5; + position: absolute; + width: calc(var(--radius) * 2); + z-index: 3; +} + +.hull { + height: 1px; + left: var(--radius); + position: absolute; + top: var(--radius); + width: 1px; +} + +.hullInner { + margin-left: calc(var(--radius) * -1); + margin-top: calc(var(--radius) * -1); +} +.hullInner > img { + border-radius: 50%; + height: calc(var(--radius) * 2); + width: calc(var(--radius) * 2); +} + +.slots { + margin-left: calc(var(--radius) - var(--radius-slots)); + margin-top: calc(var(--radius) - var(--radius-slots)); + position: relative; +} + +.slot { + height: 1px; + left: var(--radius-slots); + position: absolute; + transform-origin: 0 var(--radius-slots); + transform: rotate(var(--rotation)); + width: 1px; + z-index: 4; +} + +.slotInner { + border: 1px solid #6c6c6c; + height: calc(var(--radius-slots) / 7); + left: calc(var(--radius-slots) / 8 / 2 * -1); + position: absolute; + top: 0px; + width: calc(var(--radius-slots) / 8); +} + +.slotInnerInvalid { + border: 1px solid red; +} + +.slotItem { + --reverse-rotation: calc(-1 * var(--rotation)); + left: -32px; + position: absolute; + top: calc(-32px + var(--radius-slots) / 7 / 2); + transform: rotate(var(--reverse-rotation)) scale(calc(var(--scale) * 0.8)); +} + +.slotItem > img { + border-top-left-radius: 32px; +} diff --git a/src/ShipFit/ShipFit.stories.tsx b/src/ShipFit/ShipFit.stories.tsx new file mode 100644 index 0000000..8d5c7a7 --- /dev/null +++ b/src/ShipFit/ShipFit.stories.tsx @@ -0,0 +1,41 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { ShipFit } from './'; + +const meta: Meta = { + component: ShipFit, + tags: ['autodocs'], + title: 'Component/ShipFit', +}; + +export default meta; +type Story = StoryObj; + +const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => { + return ( + + + + + + + + ); +} + +export const Default: Story = { + args: { + radius: 365, + }, + decorators: [withShipSnapshotProvider], + parameters: { + snapshot: { + fit: {"name": "C3 Ratter : NishEM", "ship_type_id": 29984, "description": "", "items": [{"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}, {"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}, {"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": 25715}, {"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": 5, "quantity": 3720, "type_id": 24492}, {"flag": 5, "quantity": 5472, "type_id": 2679}, {"flag": 5, "quantity": 1, "type_id": 35795}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 8, "type_id": 30486}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 396, "type_id": 24492}]}, + skills: {}, + } + }, +}; diff --git a/src/ShipFit/ShipFit.tsx b/src/ShipFit/ShipFit.tsx new file mode 100644 index 0000000..29c2b46 --- /dev/null +++ b/src/ShipFit/ShipFit.tsx @@ -0,0 +1,96 @@ +import React from "react"; + +import { EveDataContext } from '../EveDataProvider'; +import { ShipSnapshotContext } from '../ShipSnapshotProvider'; + +import { Hull } from './Hull'; +import { Slot } from './Slot'; + +import styles from "./ShipFit.module.css"; + +export interface ShipFitProps { + radius?: number; +} + +/** + * Render a ship fit similar to how it is done in-game. + */ +export const ShipFit = (props: ShipFitProps) => { + const radius = props.radius ?? 365; + + const eveData = React.useContext(EveDataContext); + const shipSnapshot = React.useContext(ShipSnapshotContext); + + const scaleStyle = { + "--radius": `${radius}px`, + "--scale": `${radius / 365}` + } as React.CSSProperties; + + const slots = { + "hislot": 0, + "medslot": 0, + "lowslot": 0, + "subsystem": 0, + "rig": 0, + }; + + if (shipSnapshot?.loaded) { + slots.hislot = shipSnapshot.hull?.attributes.get(eveData?.attributeMapping?.hiSlots || 0)?.value || 0; + slots.medslot = shipSnapshot.hull?.attributes.get(eveData?.attributeMapping?.medSlots || 0)?.value || 0; + slots.lowslot = shipSnapshot.hull?.attributes.get(eveData?.attributeMapping?.lowSlots || 0)?.value || 0; + slots.subsystem = shipSnapshot.hull?.attributes.get(eveData?.attributeMapping?.maxSubSystems || 0)?.value || 0; + slots.rig = shipSnapshot.hull?.attributes.get(eveData?.attributeMapping?.rigSlots || 0)?.value || 0; + + const items = shipSnapshot.items || []; + for (const item of items) { + slots.hislot += item.attributes.get(eveData?.attributeMapping?.hiSlotModifier || 0)?.value || 0; + slots.medslot += item.attributes.get(eveData?.attributeMapping?.medSlotModifier || 0)?.value || 0; + slots.lowslot += item.attributes.get(eveData?.attributeMapping?.lowSlotModifier || 0)?.value || 0; + } + } + + return
+
+
+ + + +
+ = 1} rotation="-125deg" /> + = 2} rotation="-114deg" /> + = 3} rotation="-103deg" /> + = 4} rotation="-92deg" /> + + = 1} rotation="-73deg" /> + = 2} rotation="-63deg" /> + = 3} rotation="-53deg" /> + + = 1} rotation="-34deg" /> + = 2} rotation="-24deg" /> + = 3} rotation="-14deg" /> + = 4} rotation="-4deg" /> + = 5} rotation="6deg" /> + = 6} rotation="16deg" /> + = 7} rotation="26deg" /> + = 8} rotation="36deg" /> + + = 1} rotation="55deg" /> + = 2} rotation="65deg" /> + = 3} rotation="75deg" /> + = 4} rotation="85deg" /> + = 5} rotation="95deg" /> + = 6} rotation="105deg" /> + = 7} rotation="115deg" /> + = 8} rotation="125deg" /> + + = 1} rotation="144deg" /> + = 2} rotation="154deg" /> + = 3} rotation="164deg" /> + = 4} rotation="174deg" /> + = 5} rotation="184deg" /> + = 6} rotation="194deg" /> + = 7} rotation="204deg" /> + = 8} rotation="214deg" /> +
+
+}; diff --git a/src/ShipFit/Slot.tsx b/src/ShipFit/Slot.tsx new file mode 100644 index 0000000..2d64103 --- /dev/null +++ b/src/ShipFit/Slot.tsx @@ -0,0 +1,53 @@ +import React from "react"; +import ctlx from "clsx"; + +import { EveDataContext } from '../EveDataProvider'; +import { ShipSnapshotContext } from '../ShipSnapshotProvider'; + +import styles from "./ShipFit.module.css"; + +const esiFlagMapping: Record = { + "lowslot": [ + 11, 12, 13, 14, 15, 16, 17, 18 + ], + "medslot": [ + 19, 20, 21, 22, 23, 24, 25, 26 + ], + "hislot": [ + 27, 28, 29, 30, 31, 32, 33, 34 + ], + "rig": [ + 92, 93, 94 + ], + "subsystem": [ + 125, 126, 127, 128 + ], +}; + +export const Slot = (props: {type: string, index: number, fittable: boolean, rotation: string}) => { + const eveData = React.useContext(EveDataContext); + const shipSnapshot = React.useContext(ShipSnapshotContext); + + const rotationStyle = { "--rotation": props.rotation } as React.CSSProperties; + const esiFlag = esiFlagMapping[props.type][props.index - 1]; + + const esiItem = shipSnapshot?.fit?.items.find((item) => item.flag == esiFlag); + let item = <>; + + /* Not fittable and nothing fitted; no need to render the slot. */ + if (esiItem === undefined && !props.fittable) { + return <> + } + + if (esiItem !== undefined) { + item = + } + + return
+
+
+
+ {item} +
+
+} diff --git a/src/ShipFit/index.ts b/src/ShipFit/index.ts new file mode 100644 index 0000000..0148bbe --- /dev/null +++ b/src/ShipFit/index.ts @@ -0,0 +1 @@ +export { ShipFit } from "./ShipFit"; diff --git a/src/ShipFitExtended/ShipFitExtended.module.css b/src/ShipFitExtended/ShipFitExtended.module.css new file mode 100644 index 0000000..4dc5768 --- /dev/null +++ b/src/ShipFitExtended/ShipFitExtended.module.css @@ -0,0 +1,41 @@ +.fit { + background-color: #111111; + color: #c5c5c5; + font-size: 15px; + padding-bottom: 60px; + padding-left: 50px; + position: relative; + width: calc(var(--radius) * 2 + 2 * 50px); +} + +.cpuPg { + bottom: 0px; + position: absolute; + right: 0px; + text-align: right; +} + +.cpuPgTitle { + font-weight: bold; + margin-top: 15px; +} + +.cargoHold { + bottom: 0px; + left: 0px; + position: absolute; +} + +.cargoIcon { + display: inline-block; +} +.cargoText { + display: inline-block; + margin-left: 10px; + margin-top: 5px; + text-align: right; +} +.cargoPostfix { + display: inline-block; + margin-left: 5px; +} diff --git a/src/ShipFitExtended/ShipFitExtended.stories.tsx b/src/ShipFitExtended/ShipFitExtended.stories.tsx new file mode 100644 index 0000000..585f3a7 --- /dev/null +++ b/src/ShipFitExtended/ShipFitExtended.stories.tsx @@ -0,0 +1,41 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { ShipFitExtended } from './'; + +const meta: Meta = { + component: ShipFitExtended, + tags: ['autodocs'], + title: 'Component/ShipFitExtended', +}; + +export default meta; +type Story = StoryObj; + +const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => { + return ( + + + + + + + + ); +} + +export const Default: Story = { + args: { + radius: 365, + }, + decorators: [withShipSnapshotProvider], + parameters: { + snapshot: { + fit: {"name": "C3 Ratter : NishEM", "ship_type_id": 29984, "description": "", "items": [{"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}, {"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}, {"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": 25715}, {"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": 5, "quantity": 3720, "type_id": 24492}, {"flag": 5, "quantity": 5472, "type_id": 2679}, {"flag": 5, "quantity": 1, "type_id": 35795}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 8, "type_id": 30486}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 396, "type_id": 24492}]}, + skills: {}, + } + }, +}; diff --git a/src/ShipFitExtended/ShipFitExtended.tsx b/src/ShipFitExtended/ShipFitExtended.tsx new file mode 100644 index 0000000..c1db9a1 --- /dev/null +++ b/src/ShipFitExtended/ShipFitExtended.tsx @@ -0,0 +1,84 @@ +import React from "react"; + +import { ShipFit } from "../ShipFit"; +import { ShipAttribute } from "../ShipAttribute"; + +import styles from "./ShipFitExtended.module.css"; + +export interface ShipFitExtendedProps { + radius?: number; +} + +const CargoHold = () => { + return
+
+ C +
+
+
+ 0 +
+
+ / +
+
+
+ m3 +
+
+} + +const DroneBay = () => { + return
+
+ D +
+
+
+ 0 +
+
+ / +
+
+
+ m3 +
+
+} + +const CpuPg = (props: { title: string, children: React.ReactNode }) => { + return <> +
{props.title}
+
{props.children}
+ +} + +/** + * Render a ship fit similar to how it is done in-game. + */ +export const ShipFitExtended = (props: ShipFitExtendedProps) => { + const radius = props.radius ?? 365; + + const scaleStyle = { + "--radius": `${radius}px`, + } as React.CSSProperties; + + return
+ + +
+ + +
+ +
+ + / + + + / + +
+
+}; diff --git a/src/ShipFitExtended/index.ts b/src/ShipFitExtended/index.ts new file mode 100644 index 0000000..603b4fe --- /dev/null +++ b/src/ShipFitExtended/index.ts @@ -0,0 +1 @@ +export { ShipFitExtended } from "./ShipFitExtended"; diff --git a/src/ShipSnapshotProvider/ShipSnapshotProvider.stories.tsx b/src/ShipSnapshotProvider/ShipSnapshotProvider.stories.tsx new file mode 100644 index 0000000..9ee1bce --- /dev/null +++ b/src/ShipSnapshotProvider/ShipSnapshotProvider.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EveDataContext, EveDataProvider } from '../EveDataProvider'; +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { ShipSnapshotItemAttribute, ShipSnapshotContext, ShipSnapshotProvider } from './'; + +const meta: Meta = { + component: ShipSnapshotProvider, + tags: ['autodocs'], + title: 'Provider/ShipSnapshotProvider', +}; + +export default meta; +type Story = StoryObj; + +const TestShipSnapshot = () => { + const eveData = React.useContext(EveDataContext); + const shipSnapshot = React.useContext(ShipSnapshotContext); + + if (shipSnapshot?.loaded) { + return ( +
+ ShipSnapshot: loaded
+ Hull: +
    + {Array.from(shipSnapshot.hull?.attributes.entries() || []).map(([id, attribute]: [number, ShipSnapshotItemAttribute]) =>
  • {eveData?.dogmaAttributes?.[id].name} ({id}): {attribute.value}
  • )} +
+
+ ) + } + + return ( +
+ ShipSnapshot: loading
+
+ ); +} + +export const Default: Story = { + args: { + fit: {"name": "test", description: "", "ship_type_id": 29984, "items": [{"flag": 11, "quantity": 1, "type_id": 20639}]}, + skills: {}, + }, + render: (args) => ( + + + + + + + + ), +}; diff --git a/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx new file mode 100644 index 0000000..4dc59ce --- /dev/null +++ b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx @@ -0,0 +1,103 @@ +import React from "react"; + +import { DogmaEngineContext } from '../DogmaEngineProvider'; + +export interface ShipSnapshotItemAttribute { + base_value: number, + value: number, + effects: { + penalty: boolean, + }[], +}; + +export interface ShipSnapshotItem { + type_id: number, + attributes: Map, + effects: number[], +} + +export interface EsiFit { + name: string; + description: string; + ship_type_id: number; + items: { + flag: number; + type_id: number; + quantity: number; + }[]; +} + +interface ShipSnapshot { + loaded?: boolean; + hull?: ShipSnapshotItem; + items?: ShipSnapshotItem[]; + + fit?: EsiFit; +} + +export const ShipSnapshotContext = React.createContext({}); + +export interface ShipSnapshotProps { + /** Children that can use this provider. */ + children: React.ReactNode; + /** A ship fit in ESI representation. */ + fit: EsiFit; + /** A list of skills to apply to the fit (skill_id, skill_level). */ + skills: Record; +} + +const EsiFlagMapping = [ + 11, 12, 13, 14, 15, 16, 17, 18, // lowslot + 19, 20, 21, 22, 23, 24, 25, 26, // medslot + 27, 28, 29, 30, 31, 32, 33, 34, // hislot + 92, 93, 94, // rig + 125, 126, 127, 128, // subsystem +]; + +function esiFitToDogmaFit(fit: EsiFit): { + hull: number, + items: number[], +} { + const dogmaFit: { + hull: number, + items: number[], + } = { + "hull": fit.ship_type_id, + "items": [], + } + + for (const item of fit.items) { + if (EsiFlagMapping.includes(item.flag)) { + dogmaFit.items.push(item.type_id); + } + } + + return dogmaFit; +} + +/** + * Calculates the current attrbitues and applied effects of a ship fit. + */ +export const ShipSnapshotProvider = (props: ShipSnapshotProps) => { + const [shipSnapshot, setShipSnapshot] = React.useState({}); + const dogmaEngine = React.useContext(DogmaEngineContext); + + React.useEffect(() => { + if (!dogmaEngine.loaded) return; + if (!props.fit || !props.skills) return; + + const dogmaFit = esiFitToDogmaFit(props.fit); + const snapshot = dogmaEngine.engine?.calculate(dogmaFit, props.skills); + + setShipSnapshot({ + loaded: true, + hull: snapshot.hull, + items: snapshot.items, + fit: props.fit, + }); + }, [dogmaEngine, props.fit, props.skills]); + + return + {props.children} + +}; diff --git a/src/ShipSnapshotProvider/index.ts b/src/ShipSnapshotProvider/index.ts new file mode 100644 index 0000000..5a30071 --- /dev/null +++ b/src/ShipSnapshotProvider/index.ts @@ -0,0 +1,2 @@ +export { ShipSnapshotContext, ShipSnapshotProvider } from "./ShipSnapshotProvider"; +export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute } from "./ShipSnapshotProvider"; diff --git a/src/ShipStatistics/Category.tsx b/src/ShipStatistics/Category.tsx new file mode 100644 index 0000000..9a22810 --- /dev/null +++ b/src/ShipStatistics/Category.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +import styles from "./ShipStatistics.module.css"; + +export const Category = (props: {headerLabel: string, headerContent: React.ReactNode, children: React.ReactNode}) => { + const [expanded, setExpanded] = React.useState(true); + + return
+
setExpanded((current) => !current)} className={styles.header}> +
{props.headerLabel}
+
{props.headerContent}
+
+ +
+ {props.children} +
+
+} + +export const CategoryLine = (props: {children: React.ReactNode}) => { + return
+ {props.children} +
+} diff --git a/src/ShipStatistics/Resistance.tsx b/src/ShipStatistics/Resistance.tsx new file mode 100644 index 0000000..e963da4 --- /dev/null +++ b/src/ShipStatistics/Resistance.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { useShipAttribute } from '../ShipAttribute'; + +import styles from "./ShipStatistics.module.css"; + +export const Resistance = (props: {name: string}) => { + const stringValue = useShipAttribute({ + name: props.name, + fixed: 0, + isResistance: true, + }); + + const lname = props.name.toLowerCase(); + let type = ""; + if (lname.includes("emdamage")) { + type = "em"; + } else if (lname.includes("thermaldamage")) { + type = "thermal"; + } else if (lname.includes("kineticdamage")) { + type = "kinetic"; + } else if (lname.includes("explosivedamage")) { + type = "explosive"; + } + + return + + + {stringValue} % + +} diff --git a/src/ShipStatistics/ShipStatistics.module.css b/src/ShipStatistics/ShipStatistics.module.css new file mode 100644 index 0000000..5a43cf3 --- /dev/null +++ b/src/ShipStatistics/ShipStatistics.module.css @@ -0,0 +1,81 @@ +.panel { + background-color: #111111; + color: #c5c5c5; + font-size: 15px; + width: 350px; +} +.panel > div { + overflow-y: hidden; + transition: max-height 0.5s ease-in-out; +} + +.header { + background-color: #1d1d1d; + display: flex; + height: 25px; + line-height: 25px; + justify-content: space-between; +} + +.header > div { + flex: 1; + margin: 0px 10px; +} + +.collapsed { + max-height: 0px; +} +.expanded { + max-height: 150px; +} + + +.line { + display: flex; + justify-content: space-between; + line-height: 20px; + height: 20px; + margin: 10px 0px; +} +.line > span { + flex: 1; + margin: 0px 5px; +} + +.resistance { + background-color: #252124; + display: inline-block; + height: 20px; + margin-left: 5px; + position: relative; + width: 50px; +} +.resistance > span { + display: inline-block; + left: 0px; + position: absolute; + text-align: center; + width: 100%; + z-index: 1; +} +.resistance > .resistanceProgress { + display: inline-block; + height: 100%; + left: 0px; + position: absolute; + z-index: 0; +} + +.resistance > .resistanceProgress[data-type="em"] { + background-color: #195e8c; +} +.resistance > .resistanceProgress[data-type="thermal"] { + background-color: #8c1919; +} +.resistance > .resistanceProgress[data-type="kinetic"] { + background-color: #727272; +} +.resistance > .resistanceProgress[data-type="explosive"] { + background-color: #8c5e19; +} + diff --git a/src/ShipStatistics/ShipStatistics.stories.tsx b/src/ShipStatistics/ShipStatistics.stories.tsx new file mode 100644 index 0000000..b08b589 --- /dev/null +++ b/src/ShipStatistics/ShipStatistics.stories.tsx @@ -0,0 +1,38 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { ShipStatistics } from './'; + +const meta: Meta = { + component: ShipStatistics, + tags: ['autodocs'], + title: 'Component/ShipStatistics', +}; + +export default meta; +type Story = StoryObj; + +const withShipSnapshotProvider: Decorator> = (Story, context) => { + return ( + + + + + + + + ); +} + +export const Default: Story = { + decorators: [withShipSnapshotProvider], + parameters: { + snapshot: { + fit: JSON.parse("{\"name\": \"test\", \"ship_type_id\": 12747, \"items\": [{\"flag\": 11, \"quantity\": 1, \"type_id\": 20639}]}"), + skills: {}, + } + }, +}; diff --git a/src/ShipStatistics/ShipStatistics.tsx b/src/ShipStatistics/ShipStatistics.tsx new file mode 100644 index 0000000..6953702 --- /dev/null +++ b/src/ShipStatistics/ShipStatistics.tsx @@ -0,0 +1,127 @@ +import React from "react"; + +import { ShipAttribute } from '../ShipAttribute'; + +import { Category, CategoryLine } from "./Category"; +import { Resistance } from "./Resistance"; + +/** + * Render ship statistics similar to how it is done in-game. + */ +export const ShipStatistics = () => { + return
+ Stable + }> + + + GJ / ? s + + + + + ? GJ/s (100.0%) + + + + + ? dps + }> + + + ? dps + + + ? HP + + + + + ? ehp + }> + + No module + + + + S + hp + + + + + + + + + + + A + hp + + + + + + + + + + + S + hp + + + + + + + + + + + km + }> + + + points + + + mm + + + + + m + + + x + + + + + m/s + }> + + + t + + + x + + + + + AU/s + + + s + + + +
+}; diff --git a/src/ShipStatistics/index.ts b/src/ShipStatistics/index.ts new file mode 100644 index 0000000..fe22b28 --- /dev/null +++ b/src/ShipStatistics/index.ts @@ -0,0 +1 @@ +export { ShipStatistics } from "./ShipStatistics"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..daa3612 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +export * from './DogmaEngineProvider'; +export * from './EveDataProvider'; +export * from './FormatEftToEsi'; +export * from './ShipAttribute'; +export * from './ShipFit'; +export * from './ShipFitExtended'; +export * from './ShipSnapshotProvider'; +export * from './ShipStatistics'; diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..02b827f --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,4 @@ +declare module '*.module.css' { + const css: any; + export default css; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e99c93e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationDir": "dist/types", + "emitDeclarationOnly": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "module": "ESNext", + "moduleResolution": "node", + "outDir": "dist", + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2016", + } +}