diff --git a/src/CalculationDetail/CalculationDetail.module.css b/src/CalculationDetail/CalculationDetail.module.css new file mode 100644 index 0000000..17504cb --- /dev/null +++ b/src/CalculationDetail/CalculationDetail.module.css @@ -0,0 +1,55 @@ +.entry { + cursor: pointer; + display: flex; +} +.entry > span { + flex: 1; +} +.entry > span:first-child { + display: inline-block; + flex: unset; + width: 20px; +} + +.entry > span:last-child { + display: inline-block; + flex: unset; + width: 60px; +} +.entry:hover { + background-color: #cccccc; +} + +.header { + cursor: inherit; + font-weight: bold; +} +.header:hover { + background-color: inherit; +} + +.collapsed { + display: none; +} + +.effects { + background-color: #cccccc; + padding: 10px 20px; +} + +.effect { + display: flex; +} +.effect > span:nth-child(3) { + flex: 1; +} +.effect > span:nth-child(1) { + width: 40px; +} +.effect > span:nth-child(2) { + width: 200px; +} + +.line:nth-child(odd) { + background-color: #f2f2f2; +} diff --git a/src/CalculationDetail/CalculationDetail.stories.tsx b/src/CalculationDetail/CalculationDetail.stories.tsx new file mode 100644 index 0000000..af660c4 --- /dev/null +++ b/src/CalculationDetail/CalculationDetail.stories.tsx @@ -0,0 +1,43 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { fullFit } from '../../.storybook/fits'; + +import { DogmaEngineProvider } from '../DogmaEngineProvider'; +import { EveDataProvider } from '../EveDataProvider'; +import { ShipSnapshotProvider } from '../ShipSnapshotProvider'; +import { CalculationDetail } from './'; + +const meta: Meta = { + component: CalculationDetail, + tags: ['autodocs'], + title: 'Component/CalculationDetail', +}; + +export default meta; +type Story = StoryObj; + +const withShipSnapshotProvider: Decorator<{source: "Ship" | { Item: number }}> = (Story, context) => { + return ( + + + + + + + + ); +} + +export const Default: Story = { + args: { + source: "Ship", + }, + decorators: [withShipSnapshotProvider], + parameters: { + snapshot: { + fit: fullFit, + skills: {}, + } + }, +}; diff --git a/src/CalculationDetail/CalculationDetail.tsx b/src/CalculationDetail/CalculationDetail.tsx new file mode 100644 index 0000000..add5091 --- /dev/null +++ b/src/CalculationDetail/CalculationDetail.tsx @@ -0,0 +1,117 @@ +import clsx from "clsx"; +import React from "react"; + +import { EveDataContext } from "../EveDataProvider"; +import { ShipSnapshotContext, ShipSnapshotItemAttribute, ShipSnapshotItemAttributeEffect } from "../ShipSnapshotProvider"; + +import styles from "./CalculationDetail.module.css"; +import { Icon } from "../Icon"; + +const EffectOperatorOrder: Record = { + "PreAssign": "=", + "PreMul": "*", + "PreDiv": "/", + "ModAdd": "+", + "ModSub": "-", + "PostMul": "*", + "PostDiv": "/", + "PostPercent": "%", + "PostAssignment": "=", +}; + +const Effect = (props: { effect: ShipSnapshotItemAttributeEffect }) => { + const eveData = React.useContext(EveDataContext); + const shipSnapshot = React.useContext(ShipSnapshotContext); + + const eveAttribute = eveData.dogmaAttributes?.[props.effect.source_attribute_id]; + + let sourceName; + let attribute; + if (props.effect.source === "Ship") { + sourceName = "Ship"; + attribute = shipSnapshot.hull?.attributes.get(props.effect.source_attribute_id); + } else if (props.effect.source.Item !== undefined) { + const item = shipSnapshot.items?.[props.effect.source.Item]; + if (item === undefined) { + sourceName = "Unknown"; + } else { + sourceName = eveData.typeIDs?.[item?.type_id]?.name; + attribute = item?.attributes.get(props.effect.source_attribute_id); + } + } + + return
+ {EffectOperatorOrder[props.effect.operator]} + {attribute?.value || eveAttribute?.defaultValue}{props.effect.penalty ? " (penalized)" : ""} + {sourceName} - {eveAttribute?.name} +
; +} + +const CalculationDetailMeta = (props: { attributeId: number, attribute: ShipSnapshotItemAttribute }) => { + const [expanded, setExpanded] = React.useState(false); + const eveData = React.useContext(EveDataContext); + + const eveAttribute = eveData.dogmaAttributes?.[props.attributeId]; + let index = 0; + + const sortedEffects = props.attribute.effects.sort((a, b) => { + const aIndex = Object.keys(EffectOperatorOrder).indexOf(a.operator); + const bIndex = Object.keys(EffectOperatorOrder).indexOf(b.operator); + if (aIndex === -1 || bIndex === -1) { + return 0; + } + return aIndex - bIndex; + }); + + return
+
setExpanded(!expanded)}> + + + + {eveAttribute?.name} + {props.attribute.value} + {props.attribute.effects.length} +
+
+
+ = + {props.attribute.base_value} + base value {props.attributeId < 0 && <>(list of effects might be incomplete)} +
+ {sortedEffects.map((effect) => { + index += 1; + return + })} +
+
+} + +/** + * Show in detail for each attribute how the value came to be. This includes + * the base value, all effects (and their source) and the final value. + */ +export const CalculationDetail = (props: {source: "Ship" | { Item: number }}) => { + const shipSnapshot = React.useContext(ShipSnapshotContext); + + let attributes; + if (props.source === "Ship") { + attributes = [...shipSnapshot.hull?.attributes.entries() || []]; + } else if (props.source.Item !== undefined) { + const item = shipSnapshot.items?.[props.source.Item]; + if (item !== undefined) { + attributes = [...item.attributes.entries()]; + } + } + + return
+
+ + Attribute + Value + Effects +
+ {attributes?.map(([attributeId, attribute]) => { + return + })} +
+}; diff --git a/src/CalculationDetail/index.ts b/src/CalculationDetail/index.ts new file mode 100644 index 0000000..1a223d2 --- /dev/null +++ b/src/CalculationDetail/index.ts @@ -0,0 +1 @@ +export { CalculationDetail } from "./CalculationDetail"; diff --git a/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx index a506f5d..b74947e 100644 --- a/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx +++ b/src/ShipSnapshotProvider/ShipSnapshotProvider.tsx @@ -2,12 +2,18 @@ import React from "react"; import { DogmaEngineContext } from '../DogmaEngineProvider'; +export interface ShipSnapshotItemAttributeEffect { + operator: string, + penalty: boolean, + source: "Ship" | { Item: number }, + source_category: string, + source_attribute_id: number, +}; + export interface ShipSnapshotItemAttribute { base_value: number, value: number, - effects: { - penalty: boolean, - }[], + effects: ShipSnapshotItemAttributeEffect[], }; export interface ShipSnapshotItem { diff --git a/src/ShipSnapshotProvider/index.ts b/src/ShipSnapshotProvider/index.ts index 5a30071..c498879 100644 --- a/src/ShipSnapshotProvider/index.ts +++ b/src/ShipSnapshotProvider/index.ts @@ -1,2 +1,2 @@ export { ShipSnapshotContext, ShipSnapshotProvider } from "./ShipSnapshotProvider"; -export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute } from "./ShipSnapshotProvider"; +export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute, ShipSnapshotItemAttributeEffect } from "./ShipSnapshotProvider"; diff --git a/src/index.ts b/src/index.ts index 85bbf75..77e828e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export * from './DogmaEngineProvider'; +export * from './CalculationDetail'; export * from './EsiCharacterSelection'; export * from './EsiProvider'; export * from './EveDataProvider';