feat: component to show in detail how attributes were calculated (#31)

This includes which effects applied on them, where they came from, what
their value was, etc.
This commit is contained in:
Patric Stout
2023-12-01 14:45:29 +01:00
committed by GitHub
parent 698795ca78
commit 2f7dae6735
7 changed files with 227 additions and 4 deletions

View File

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

View File

@@ -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<typeof CalculationDetail> = {
component: CalculationDetail,
tags: ['autodocs'],
title: 'Component/CalculationDetail',
};
export default meta;
type Story = StoryObj<typeof CalculationDetail>;
const withShipSnapshotProvider: Decorator<{source: "Ship" | { Item: number }}> = (Story, context) => {
return (
<EveDataProvider>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<Story {...context.args} />
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
);
}
export const Default: Story = {
args: {
source: "Ship",
},
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: fullFit,
skills: {},
}
},
};

View File

@@ -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<string, string> = {
"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 <div className={styles.effect}>
<span>{EffectOperatorOrder[props.effect.operator]}</span>
<span>{attribute?.value || eveAttribute?.defaultValue}{props.effect.penalty ? " (penalized)" : ""}</span>
<span>{sourceName} - {eveAttribute?.name}</span>
</div>;
}
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 <div className={styles.line}>
<div className={styles.entry} onClick={() => setExpanded(!expanded)}>
<span>
<Icon name={expanded ? "menu-expand" : "menu-collapse"} />
</span>
<span>{eveAttribute?.name}</span>
<span>{props.attribute.value}</span>
<span>{props.attribute.effects.length}</span>
</div>
<div className={clsx(styles.effects, { [styles.collapsed]: !expanded })}>
<div className={styles.effect}>
<span>=</span>
<span>{props.attribute.base_value}</span>
<span>base value {props.attributeId < 0 && <>(list of effects might be incomplete)</>}</span>
</div>
{sortedEffects.map((effect) => {
index += 1;
return <Effect key={index} effect={effect} />
})}
</div>
</div>
}
/**
* 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 <div>
<div className={clsx(styles.entry, styles.header)}>
<span></span>
<span>Attribute</span>
<span>Value</span>
<span>Effects</span>
</div>
{attributes?.map(([attributeId, attribute]) => {
return <CalculationDetailMeta key={attributeId} attributeId={attributeId} attribute={attribute} />
})}
</div>
};

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
export { ShipSnapshotContext, ShipSnapshotProvider } from "./ShipSnapshotProvider";
export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute } from "./ShipSnapshotProvider";
export type { EsiFit, ShipSnapshotItem, ShipSnapshotItemAttribute, ShipSnapshotItemAttributeEffect } from "./ShipSnapshotProvider";

View File

@@ -1,4 +1,5 @@
export * from './DogmaEngineProvider';
export * from './CalculationDetail';
export * from './EsiCharacterSelection';
export * from './EsiProvider';
export * from './EveDataProvider';