feat: on hover, preview the module / charge (#138)
This shows the impact adding the module / charge would have, and colouring the statistics accordingly.
This commit is contained in:
@@ -23,10 +23,10 @@ export const RenameButton = () => {
|
||||
}, [fitManager]);
|
||||
|
||||
const openRename = React.useCallback(() => {
|
||||
if (currentFit.fit === null) return;
|
||||
setName(currentFit.fit.name);
|
||||
if (currentFit.currentFit === null) return;
|
||||
setName(currentFit.currentFit.name);
|
||||
setIsRenameOpen(true);
|
||||
}, [currentFit.fit]);
|
||||
}, [currentFit.currentFit]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -16,13 +16,13 @@ export const SaveButton = () => {
|
||||
|
||||
const saveBrowser = React.useCallback(
|
||||
(force?: boolean) => {
|
||||
if (currentFit.fit === null) return;
|
||||
if (currentFit.currentFit === null) return;
|
||||
|
||||
setIsPopupOpen(false);
|
||||
|
||||
if (!force) {
|
||||
for (const fit of localFits.fittings) {
|
||||
if (fit.name === currentFit.fit.name) {
|
||||
if (fit.name === currentFit.currentFit.name) {
|
||||
setIsAlreadyExistsOpen(true);
|
||||
return;
|
||||
}
|
||||
@@ -30,9 +30,9 @@ export const SaveButton = () => {
|
||||
}
|
||||
|
||||
setIsAlreadyExistsOpen(false);
|
||||
localFits.addFit(currentFit.fit);
|
||||
localFits.addFit(currentFit.currentFit);
|
||||
},
|
||||
[localFits, currentFit.fit],
|
||||
[localFits, currentFit.currentFit],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -60,7 +60,7 @@ export const SaveButton = () => {
|
||||
>
|
||||
<div>
|
||||
<div>
|
||||
You have a local fitting with the name "{currentFit.fit?.name}"; do you want to update it?
|
||||
You have a local fitting with the name "{currentFit.currentFit?.name}"; do you want to update it?
|
||||
</div>
|
||||
<div className={styles.alreadyExistsButtons}>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveBrowser(true)}>
|
||||
|
||||
@@ -21,7 +21,7 @@ type Story = StoryObj<StoryProps>;
|
||||
const TestStory = () => {
|
||||
const currentFit = useCurrentFit();
|
||||
|
||||
return <div>Current Fit: {currentFit.fit?.name}</div>;
|
||||
return <div>Current Fit: {currentFit.currentFit?.name}</div>;
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
|
||||
@@ -27,12 +27,14 @@ export const FitHistory = (props: FitHistoryProps) => {
|
||||
historySizeRef.current = props.historySize;
|
||||
|
||||
React.useEffect(() => {
|
||||
const fit = currentFit.fit;
|
||||
if (fit === null) return;
|
||||
const fit = currentFit.currentFit;
|
||||
if (fit === null || currentFit.isPreview) return;
|
||||
/* Store the fit as a JSON string, to ensure that any modifications
|
||||
* to the current doesn't impact the history. */
|
||||
const fitString = JSON.stringify(fit);
|
||||
/* Do not store fits in the history that have no changes. */
|
||||
if (currentIndexRef.current !== -1 && historyRef.current[currentIndexRef.current] === fitString) return;
|
||||
if (historyRef.current.length > 0 && historyRef.current[historyRef.current.length - 1] === fitString) return;
|
||||
|
||||
setHistory((prev) => {
|
||||
if (prev.length >= historySizeRef.current) {
|
||||
@@ -42,7 +44,7 @@ export const FitHistory = (props: FitHistoryProps) => {
|
||||
return [...prev, fitString];
|
||||
});
|
||||
setCurrentIndex(-1);
|
||||
}, [currentFit.fit]);
|
||||
}, [currentFit.currentFit, currentFit.isPreview]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentIndex === -1) return;
|
||||
|
||||
@@ -56,16 +56,21 @@ const OnItemDragStart = (
|
||||
};
|
||||
};
|
||||
|
||||
const PreloadImage = (typeId: number): ((e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void) => {
|
||||
return () => {
|
||||
const img = new Image();
|
||||
img.src = `https://images.evetech.net/types/${typeId}/icon?size=64`;
|
||||
};
|
||||
};
|
||||
|
||||
const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: boolean }) => {
|
||||
const fitManager = useFitManager();
|
||||
|
||||
const PreviewStart = React.useCallback(
|
||||
(typeId: number, slotType: CalculationSlotType): void => {
|
||||
if (slotType === "DroneBay") return;
|
||||
fitManager.addItem(typeId, slotType, true);
|
||||
},
|
||||
[fitManager],
|
||||
);
|
||||
|
||||
const PreviewEnd = React.useCallback((): void => {
|
||||
fitManager.removePreview();
|
||||
}, [fitManager]);
|
||||
|
||||
const getChildren = React.useCallback(() => {
|
||||
return (
|
||||
<>
|
||||
@@ -78,9 +83,13 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo
|
||||
key={item.typeId}
|
||||
level={2}
|
||||
content={item.name}
|
||||
onDoubleClick={() => fitManager.addItem(item.typeId, slotType)}
|
||||
onDoubleClick={() => {
|
||||
PreviewEnd();
|
||||
fitManager.addItem(item.typeId, slotType);
|
||||
}}
|
||||
onDragStart={OnItemDragStart(item.typeId, slotType)}
|
||||
onMouseEnter={PreloadImage(item.typeId)}
|
||||
onMouseEnter={() => PreviewStart(item.typeId, slotType)}
|
||||
onMouseLeave={() => PreviewEnd()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -95,7 +104,7 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}, [fitManager, props.group, props.level]);
|
||||
}, [fitManager, props.group, props.level, PreviewStart, PreviewEnd]);
|
||||
|
||||
if (props.hideGroup) {
|
||||
return <TreeListing level={props.level} getChildren={getChildren} />;
|
||||
|
||||
@@ -213,7 +213,7 @@ export const HullListing = () => {
|
||||
if (hull.marketGroupID === undefined) continue;
|
||||
if (!hull.published) continue;
|
||||
|
||||
if (filter.currentHull && currentFit.fit?.shipTypeId !== parseInt(typeId)) continue;
|
||||
if (filter.currentHull && currentFit.currentFit?.shipTypeId !== parseInt(typeId)) continue;
|
||||
|
||||
const fits: ListingFit[] = [];
|
||||
if (anyFilter) {
|
||||
@@ -221,7 +221,7 @@ export const HullListing = () => {
|
||||
if (filter.characterFits && Object.keys(characterFitsGrouped).includes(typeId))
|
||||
fits.push(...characterFitsGrouped[typeId]);
|
||||
if (fits.length == 0) {
|
||||
if (!filter.currentHull || currentFit.fit?.shipTypeId !== parseInt(typeId)) continue;
|
||||
if (!filter.currentHull || currentFit.currentFit?.shipTypeId !== parseInt(typeId)) continue;
|
||||
}
|
||||
} else {
|
||||
if (Object.keys(localFitsGrouped).includes(typeId)) fits.push(...localFitsGrouped[typeId]);
|
||||
|
||||
7
src/components/ShipAttribute/ShipAttribute.module.css
Normal file
7
src/components/ShipAttribute/ShipAttribute.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.increase {
|
||||
color: #8dc169;
|
||||
}
|
||||
|
||||
.decrease {
|
||||
color: #ff454b;
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { useEveData } from "@/providers/EveDataProvider";
|
||||
import { useStatistics } from "@/providers/StatisticsProvider";
|
||||
import { useCurrentStatistics, useStatistics } from "@/providers/StatisticsProvider";
|
||||
|
||||
import styles from "./ShipAttribute.module.css";
|
||||
|
||||
export interface AttributeProps {
|
||||
/** Name of the attribute. */
|
||||
name: string;
|
||||
/** The unit of the attribute, used as postfix. */
|
||||
unit?: string;
|
||||
/** How many decimals to render. */
|
||||
fixed: number;
|
||||
/** Whether this is a resistance attribute. */
|
||||
@@ -16,28 +21,55 @@ export interface AttributeProps {
|
||||
roundDown?: boolean;
|
||||
}
|
||||
|
||||
export enum AttributeChange {
|
||||
Increase = "Increase",
|
||||
Decrease = "Decrease",
|
||||
Unchanged = "Unchanged",
|
||||
Unknown = "Unknown",
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of a ship's attribute.
|
||||
*/
|
||||
export function useAttribute(type: "Ship" | "Char", props: AttributeProps) {
|
||||
export function useAttribute(type: "Ship" | "Char", props: AttributeProps): { value: string; change: AttributeChange } {
|
||||
const eveData = useEveData();
|
||||
const statistics = useStatistics();
|
||||
const currentStatistics = useCurrentStatistics();
|
||||
|
||||
const attributeId = eveData?.attributeMapping[props.name] ?? 0;
|
||||
|
||||
let value;
|
||||
let currentValue;
|
||||
if (eveData === null || statistics === null) {
|
||||
value = 0;
|
||||
currentValue = 0;
|
||||
} else {
|
||||
const attributeId = eveData.attributeMapping[props.name] ?? 0;
|
||||
|
||||
if (type === "Ship") {
|
||||
value = statistics.hull.attributes.get(attributeId)?.value;
|
||||
currentValue = currentStatistics?.hull.attributes.get(attributeId)?.value;
|
||||
} else {
|
||||
value = statistics.char.attributes.get(attributeId)?.value;
|
||||
currentValue = currentStatistics?.char.attributes.get(attributeId)?.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (value === undefined) {
|
||||
return "?";
|
||||
return {
|
||||
value: "?",
|
||||
change: AttributeChange.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
let change = AttributeChange.Unchanged;
|
||||
if (currentValue !== undefined && currentValue !== value) {
|
||||
const highIsGood =
|
||||
props.isResistance || props.name === "mass" ? false : eveData?.dogmaAttributes[attributeId]?.highIsGood;
|
||||
|
||||
if (currentValue < value) {
|
||||
change = highIsGood ? AttributeChange.Increase : AttributeChange.Decrease;
|
||||
} else {
|
||||
change = highIsGood ? AttributeChange.Decrease : AttributeChange.Increase;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.isResistance) {
|
||||
@@ -65,26 +97,61 @@ export function useAttribute(type: "Ship" | "Char", props: AttributeProps) {
|
||||
value = 0;
|
||||
}
|
||||
|
||||
return value.toLocaleString("en", {
|
||||
minimumFractionDigits: props.fixed,
|
||||
maximumFractionDigits: props.fixed,
|
||||
});
|
||||
return {
|
||||
value: value.toLocaleString("en", {
|
||||
minimumFractionDigits: props.fixed,
|
||||
maximumFractionDigits: props.fixed,
|
||||
}),
|
||||
change,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single ship attribute of a ship's snapshot.
|
||||
*/
|
||||
export const ShipAttribute = (props: AttributeProps) => {
|
||||
const stringValue = useAttribute("Ship", props);
|
||||
const { value, change } = useAttribute("Ship", props);
|
||||
const prefix =
|
||||
props.unit === undefined
|
||||
? ""
|
||||
: props.unit === "s" || props.unit === "x" || props.unit === "%"
|
||||
? props.unit
|
||||
: ` ${props.unit}`;
|
||||
|
||||
return <span>{stringValue}</span>;
|
||||
return (
|
||||
<span
|
||||
className={clsx({
|
||||
[styles.increase]: change === AttributeChange.Increase,
|
||||
[styles.decrease]: change === AttributeChange.Decrease,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
{prefix}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a single character attribute of a ship's snapshot.
|
||||
*/
|
||||
export const CharAttribute = (props: AttributeProps) => {
|
||||
const stringValue = useAttribute("Char", props);
|
||||
const { value, change } = useAttribute("Char", props);
|
||||
const prefix =
|
||||
props.unit === undefined
|
||||
? ""
|
||||
: props.unit === "s" || props.unit === "x" || props.unit === "%"
|
||||
? props.unit
|
||||
: ` ${props.unit}`;
|
||||
|
||||
return <span>{stringValue}</span>;
|
||||
return (
|
||||
<span
|
||||
className={clsx({
|
||||
[styles.increase]: change === AttributeChange.Increase,
|
||||
[styles.decrease]: change === AttributeChange.Decrease,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
{prefix}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,11 +10,11 @@ export interface ShipFitProps {
|
||||
|
||||
export const Hull = () => {
|
||||
const currentFit = useCurrentFit();
|
||||
if (currentFit.fit === null) {
|
||||
if (currentFit.currentFit === null) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const shipTypeId = currentFit.fit.shipTypeId;
|
||||
const shipTypeId = currentFit.currentFit.shipTypeId;
|
||||
if (shipTypeId === undefined) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -209,3 +209,7 @@
|
||||
transform: rotate(var(--reverse-rotation));
|
||||
stroke: #ffffff;
|
||||
}
|
||||
|
||||
.preview {
|
||||
filter: sepia(100%) hue-rotate(190deg) saturate(200%);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { Icon, IconName } from "@/components/Icon";
|
||||
@@ -5,7 +6,7 @@ import { useEveData } from "@/providers/EveDataProvider";
|
||||
import { useStatistics } from "@/providers/StatisticsProvider";
|
||||
import { useFitManager } from "@/providers/FitManagerProvider";
|
||||
import { CalculationSlot } from "@/providers/DogmaEngineProvider";
|
||||
import { EsfSlot, EsfSlotType, EsfState } from "@/providers/CurrentFitProvider";
|
||||
import { EsfSlot, EsfSlotType, EsfState, useCurrentFit } from "@/providers/CurrentFitProvider";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
@@ -20,8 +21,12 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea
|
||||
const eveData = useEveData();
|
||||
const statistics = useStatistics();
|
||||
const fitManager = useFitManager();
|
||||
const currentFit = useCurrentFit();
|
||||
|
||||
const module = statistics?.items.find((item) => item.slot.type === props.type && item.slot.index === props.index);
|
||||
const fitModule = currentFit?.fit?.modules.find(
|
||||
(item) => item.slot.type === props.type && item.slot.index === props.index,
|
||||
);
|
||||
const active = module?.max_state !== "Passive" && module?.max_state !== "Online";
|
||||
|
||||
const offlineState = React.useCallback(
|
||||
@@ -199,8 +204,12 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea
|
||||
preserveAspectRatio="xMidYMin slice"
|
||||
>
|
||||
<use href="#slot" />
|
||||
{props.fittable && module !== undefined && active && <use href="#slot-active" />}
|
||||
{props.fittable && module !== undefined && !active && <use href="#slot-passive" />}
|
||||
{props.fittable && fitModule?.state !== "Preview" && module !== undefined && active && (
|
||||
<use href="#slot-active" />
|
||||
)}
|
||||
{props.fittable && fitModule?.state !== "Preview" && module !== undefined && !active && (
|
||||
<use href="#slot-passive" />
|
||||
)}
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
@@ -233,6 +242,7 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea
|
||||
title={eveData.typeIDs[module.type_id].name}
|
||||
draggable={true}
|
||||
onDragStart={onDragStart}
|
||||
className={clsx({ [styles.preview]: fitModule?.state === "Preview" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useFitSelection, withDecoratorFull } from "../../../.storybook/helpers"
|
||||
|
||||
import { HardwareListing } from "@/components/HardwareListing";
|
||||
import { HullListing } from "@/components/HullListing";
|
||||
import { ShipStatistics } from "@/components/ShipStatistics";
|
||||
import { EsfFit } from "@/providers/CurrentFitProvider";
|
||||
|
||||
import { ShipFitExtended } from "./";
|
||||
@@ -52,7 +53,7 @@ export const WithHardwareListing: Story = {
|
||||
useFitSelection(fit);
|
||||
|
||||
return (
|
||||
<div style={{ width: 1230, height: 730, display: "flex" }}>
|
||||
<div style={{ width: 1530, height: 730, display: "flex" }}>
|
||||
<div style={{ width: 400 }}>
|
||||
<HardwareListing />
|
||||
</div>
|
||||
@@ -60,6 +61,10 @@ export const WithHardwareListing: Story = {
|
||||
<div style={{ width: 730, height: 730 }}>
|
||||
<ShipFitExtended {...args} />
|
||||
</div>
|
||||
<div style={{ width: 100 }}></div>
|
||||
<div style={{ width: 200 }}>
|
||||
<ShipStatistics />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -77,7 +82,7 @@ export const WithHullListing: Story = {
|
||||
useFitSelection(fit);
|
||||
|
||||
return (
|
||||
<div style={{ width: 1230, height: 730, display: "flex" }}>
|
||||
<div style={{ width: 1530, height: 730, display: "flex" }}>
|
||||
<div style={{ width: 400 }}>
|
||||
<HullListing />
|
||||
</div>
|
||||
@@ -85,6 +90,10 @@ export const WithHullListing: Story = {
|
||||
<div style={{ width: 730, height: 730 }}>
|
||||
<ShipFitExtended {...args} />
|
||||
</div>
|
||||
<div style={{ width: 100 }}></div>
|
||||
<div style={{ width: 200 }}>
|
||||
<ShipStatistics />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -36,7 +36,7 @@ const ShipDroneBay = () => {
|
||||
|
||||
if (eveData === null) return <></>;
|
||||
|
||||
const isStructure = eveData.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65;
|
||||
const isStructure = eveData.typeIDs[currentFit.currentFit?.shipTypeId ?? 0]?.categoryID === 65;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -77,7 +77,7 @@ const FitName = () => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.fitNameTitle}>Name</div>
|
||||
<div className={styles.fitNameContent}>{currentFit.fit?.name}</div>
|
||||
<div className={styles.fitNameContent}>{currentFit.currentFit?.name}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -118,7 +118,7 @@ export const ShipFitExtended = () => {
|
||||
</CpuPg>
|
||||
</div>
|
||||
|
||||
{currentFit.fit === null && <div className={styles.empty}>To start, select a hull on the left.</div>}
|
||||
{currentFit.currentFit === null && <div className={styles.empty}>To start, select a hull on the left.</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,17 +2,17 @@ import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import { IconName, Icon } from "@/components/Icon";
|
||||
import { useAttribute } from "@/components/ShipAttribute";
|
||||
import { ShipAttribute, useAttribute } from "@/components/ShipAttribute";
|
||||
|
||||
import styles from "./ShipStatistics.module.css";
|
||||
|
||||
export const RechargeRateItem = (props: { name: string; icon: IconName }) => {
|
||||
const stringValue = useAttribute("Ship", {
|
||||
const { value } = useAttribute("Ship", {
|
||||
name: props.name,
|
||||
fixed: 1,
|
||||
});
|
||||
|
||||
if (stringValue == "0.0") {
|
||||
if (value == "0.0") {
|
||||
return (
|
||||
<span className={styles.statistic}>
|
||||
<span>
|
||||
@@ -28,7 +28,7 @@ export const RechargeRateItem = (props: { name: string; icon: IconName }) => {
|
||||
<span>
|
||||
<Icon name={props.icon} size={24} />
|
||||
</span>
|
||||
<span>{stringValue} hp/s</span>
|
||||
<ShipAttribute name={props.name} fixed={1} unit="hp/s" />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import { useAttribute } from "@/components/ShipAttribute";
|
||||
import { ShipAttribute, useAttribute } from "@/components/ShipAttribute";
|
||||
|
||||
import styles from "./ShipStatistics.module.css";
|
||||
|
||||
export const Resistance = (props: { name: string }) => {
|
||||
const stringValue = useAttribute("Ship", {
|
||||
const { value } = useAttribute("Ship", {
|
||||
name: props.name,
|
||||
fixed: 0,
|
||||
isResistance: true,
|
||||
@@ -25,8 +25,8 @@ export const Resistance = (props: { name: string }) => {
|
||||
|
||||
return (
|
||||
<span className={styles.resistance}>
|
||||
<span className={styles.resistanceProgress} data-type={type} style={{ width: `${stringValue}%` }}></span>
|
||||
<span>{stringValue} %</span>
|
||||
<span className={styles.resistanceProgress} data-type={type} style={{ width: `${value}%` }}></span>
|
||||
<ShipAttribute name={props.name} fixed={0} unit=" %" isResistance={true} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ export const ShipStatistics = () => {
|
||||
const statistics = useStatistics();
|
||||
|
||||
let capacitorState = "Stable";
|
||||
const isStructure = eveData?.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65;
|
||||
const isStructure = eveData?.typeIDs[currentFit.currentFit?.shipTypeId ?? 0]?.categoryID === 65;
|
||||
|
||||
const attributeId = eveData?.attributeMapping.capacitorDepletesIn ?? 0;
|
||||
const capacitorDepletesIn = statistics?.hull.attributes.get(attributeId)?.value;
|
||||
@@ -55,15 +55,14 @@ export const ShipStatistics = () => {
|
||||
>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
<ShipAttribute name="capacitorCapacity" fixed={1} /> GJ /{" "}
|
||||
<ShipAttribute name="rechargeRate" fixed={2} divideBy={1000} /> s
|
||||
<ShipAttribute name="capacitorCapacity" fixed={1} unit="GJ" /> /{" "}
|
||||
<ShipAttribute name="rechargeRate" fixed={2} divideBy={1000} unit=" s" />
|
||||
</span>
|
||||
</CategoryLine>
|
||||
<CategoryLine>
|
||||
<span>
|
||||
Δ <ShipAttribute name="capacitorPeakDelta" fixed={1} /> GJ/s (
|
||||
<ShipAttribute name="capacitorPeakDeltaPercentage" fixed={1} />
|
||||
%)
|
||||
Δ <ShipAttribute name="capacitorPeakDelta" fixed={1} unit="GJ/s" /> (
|
||||
<ShipAttribute name="capacitorPeakDeltaPercentage" fixed={1} unit="%" />)
|
||||
</span>
|
||||
</CategoryLine>
|
||||
</Category>
|
||||
@@ -72,7 +71,7 @@ export const ShipStatistics = () => {
|
||||
headerLabel="Offense"
|
||||
headerContent={
|
||||
<span>
|
||||
<ShipAttribute name="damageWithoutReloadDps" fixed={1} /> dps
|
||||
<ShipAttribute name="damageWithoutReloadDps" fixed={1} unit="dps" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -82,8 +81,8 @@ export const ShipStatistics = () => {
|
||||
<Icon name="damage-dps" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="damageWithoutReloadDps" fixed={1} /> dps (
|
||||
<ShipAttribute name="damageWithReloadDps" fixed={1} /> dps)
|
||||
<ShipAttribute name="damageWithoutReloadDps" fixed={1} unit="dps" /> (
|
||||
<ShipAttribute name="damageWithReloadDps" fixed={1} unit="dps" />)
|
||||
</span>
|
||||
</span>
|
||||
<span title="Alpha Strike" className={styles.statistic}>
|
||||
@@ -91,7 +90,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="damage-alpha" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="damageAlphaHp" fixed={0} /> HP
|
||||
<ShipAttribute name="damageAlphaHp" fixed={0} unit="HP" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
@@ -101,7 +100,7 @@ export const ShipStatistics = () => {
|
||||
headerLabel="Defense"
|
||||
headerContent={
|
||||
<span>
|
||||
<ShipAttribute name="ehp" fixed={0} roundDown /> ehp
|
||||
<ShipAttribute name="ehp" fixed={0} roundDown unit="ehp" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -128,9 +127,10 @@ export const ShipStatistics = () => {
|
||||
<Icon name="shield-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="shieldCapacity" fixed={0} roundDown /> hp
|
||||
<ShipAttribute name="shieldCapacity" fixed={0} roundDown unit="hp" />
|
||||
<br />
|
||||
<ShipAttribute name="shieldRechargeRate" fixed={0} divideBy={1000} roundDown unit="s" />
|
||||
<br />
|
||||
<ShipAttribute name="shieldRechargeRate" fixed={0} divideBy={1000} roundDown /> s<br />
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ flex: 2 }}>
|
||||
@@ -146,7 +146,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="armor-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="armorHP" fixed={0} roundDown /> hp
|
||||
<ShipAttribute name="armorHP" fixed={0} roundDown unit="hp" />
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ flex: 2 }}>
|
||||
@@ -162,7 +162,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="hull-hp" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="hp" fixed={0} roundDown /> hp
|
||||
<ShipAttribute name="hp" fixed={0} roundDown unit="hp" />
|
||||
</span>
|
||||
</span>
|
||||
<span style={{ flex: 2 }}>
|
||||
@@ -178,7 +178,7 @@ export const ShipStatistics = () => {
|
||||
headerLabel="Targeting"
|
||||
headerContent={
|
||||
<span>
|
||||
<ShipAttribute name="maxTargetRange" fixed={2} divideBy={1000} /> km
|
||||
<ShipAttribute name="maxTargetRange" fixed={2} divideBy={1000} unit="km" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -188,7 +188,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="sensor-strength" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanStrength" fixed={2} /> points
|
||||
<ShipAttribute name="scanStrength" fixed={2} unit="points" />
|
||||
</span>
|
||||
</span>
|
||||
<span title="Scan Resolution" className={styles.statistic}>
|
||||
@@ -196,7 +196,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="scan-resolution" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="scanResolution" fixed={0} /> mm
|
||||
<ShipAttribute name="scanResolution" fixed={0} unit="mm" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
@@ -206,7 +206,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="signature-radius" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="signatureRadius" fixed={0} /> m
|
||||
<ShipAttribute name="signatureRadius" fixed={0} unit="m" />
|
||||
</span>
|
||||
</span>
|
||||
<span title="Maximum Locked Targets" className={styles.statistic}>
|
||||
@@ -214,7 +214,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="maximum-locked-targets" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="maxLockedTargets" fixed={0} />x
|
||||
<ShipAttribute name="maxLockedTargets" fixed={0} unit="x" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
@@ -225,7 +225,7 @@ export const ShipStatistics = () => {
|
||||
headerLabel="Navigation"
|
||||
headerContent={
|
||||
<span>
|
||||
<ShipAttribute name="maxVelocity" fixed={1} /> m/s
|
||||
<ShipAttribute name="maxVelocity" fixed={1} unit="m/s" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -235,7 +235,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="mass" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="mass" fixed={2} divideBy={1000} /> t
|
||||
<ShipAttribute name="mass" fixed={2} divideBy={1000} unit="t" />
|
||||
</span>
|
||||
</span>
|
||||
<span title="Inertia Modifier" className={styles.statistic}>
|
||||
@@ -243,7 +243,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="inertia-modifier" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="agility" fixed={4} />x
|
||||
<ShipAttribute name="agility" fixed={4} unit="x" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
@@ -253,7 +253,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="warp-speed" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="warpSpeedMultiplier" fixed={2} /> AU/s
|
||||
<ShipAttribute name="warpSpeedMultiplier" fixed={2} unit="AU/s" />
|
||||
</span>
|
||||
</span>
|
||||
<span title="Align Time" className={styles.statistic}>
|
||||
@@ -261,7 +261,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="align-time" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<ShipAttribute name="alignTime" fixed={2} />s
|
||||
<ShipAttribute name="alignTime" fixed={2} unit="s" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
@@ -273,7 +273,7 @@ export const ShipStatistics = () => {
|
||||
headerLabel="Drones"
|
||||
headerContent={
|
||||
<span>
|
||||
<ShipAttribute name="droneDamageDps" fixed={1} /> dps
|
||||
<ShipAttribute name="droneDamageDps" fixed={1} unit="dps" />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
@@ -292,7 +292,7 @@ export const ShipStatistics = () => {
|
||||
<Icon name="inertia-modifier" size={24} />
|
||||
</span>
|
||||
<span>
|
||||
<CharAttribute name="droneControlDistance" fixed={2} divideBy={1000} /> km
|
||||
<CharAttribute name="droneControlDistance" fixed={2} divideBy={1000} unit="km" />
|
||||
</span>
|
||||
</span>
|
||||
</CategoryLine>
|
||||
|
||||
@@ -56,6 +56,7 @@ export const TreeLeaf = (props: {
|
||||
onDoubleClick?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onDragStart?: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
}) => {
|
||||
const stylesHeader = styles[`header${props.level}`];
|
||||
|
||||
@@ -76,6 +77,7 @@ export const TreeLeaf = (props: {
|
||||
draggable={!!props.onDragStart}
|
||||
onDragStart={props.onDragStart}
|
||||
onMouseEnter={props.onMouseEnter}
|
||||
onMouseLeave={props.onMouseLeave}
|
||||
>
|
||||
{props.icon !== undefined && (
|
||||
<span className={styles.leafIcon}>
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useExportEft() {
|
||||
const statistics = useStatistics();
|
||||
|
||||
return (): string | null => {
|
||||
const fit = currentFit.fit;
|
||||
const fit = currentFit.currentFit;
|
||||
|
||||
if (eveData === null || fit === null || statistics === null) return null;
|
||||
|
||||
|
||||
@@ -55,8 +55,8 @@ export function useExportEveShipFitHash(hashOnly?: boolean) {
|
||||
setFitHash((hashOnly ? "" : "https://eveship.fit/") + `#fit:${newFitHash}`);
|
||||
}
|
||||
|
||||
createHash(currentFit.fit);
|
||||
}, [currentFit.fit, hashOnly]);
|
||||
createHash(currentFit.currentFit);
|
||||
}, [currentFit.currentFit, hashOnly]);
|
||||
|
||||
return fitHash;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const TestStory = ({ fit }: { fit: EsfFit | null }) => {
|
||||
currentFit.setFit(fit ?? null);
|
||||
});
|
||||
|
||||
return <pre>{JSON.stringify(currentFit.fit, null, 2)}</pre>;
|
||||
return <pre>{JSON.stringify(currentFit.currentFit, null, 2)}</pre>;
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface EsfSlot {
|
||||
export interface EsfModule {
|
||||
typeId: number;
|
||||
slot: EsfSlot;
|
||||
state: EsfState;
|
||||
state: EsfState | "Preview";
|
||||
charge?: EsfCharge;
|
||||
}
|
||||
|
||||
@@ -42,13 +42,24 @@ export interface EsfFit {
|
||||
}
|
||||
|
||||
interface CurrentFit {
|
||||
/** The current fit to render. */
|
||||
fit: EsfFit | null;
|
||||
/** The current fit, regardless of preview state. */
|
||||
currentFit: EsfFit | null;
|
||||
/** Whether the fit is a preview. */
|
||||
isPreview: boolean;
|
||||
/** Set the current fit. */
|
||||
setFit: React.Dispatch<React.SetStateAction<EsfFit | null>>;
|
||||
/** Set a (temporary) preview fit, for the over-over effect on modules. */
|
||||
setPreview: React.Dispatch<React.SetStateAction<EsfFit | null>>;
|
||||
}
|
||||
|
||||
const CurrentFitContext = React.createContext<CurrentFit>({
|
||||
fit: null,
|
||||
currentFit: null,
|
||||
isPreview: false,
|
||||
setFit: () => {},
|
||||
setPreview: () => {},
|
||||
});
|
||||
|
||||
export const useCurrentFit = () => {
|
||||
@@ -73,13 +84,17 @@ interface CurrentFitProps {
|
||||
*/
|
||||
export const CurrentFitProvider = (props: CurrentFitProps) => {
|
||||
const [currentFit, setCurrentFit] = React.useState<EsfFit | null>(props.initialFit ?? null);
|
||||
const [previewFit, setPreviewFit] = React.useState<EsfFit | null>(null);
|
||||
|
||||
const contextValue = React.useMemo(() => {
|
||||
return {
|
||||
fit: currentFit,
|
||||
fit: previewFit ?? currentFit,
|
||||
currentFit: currentFit,
|
||||
isPreview: previewFit !== null,
|
||||
setFit: setCurrentFit,
|
||||
setPreview: setPreviewFit,
|
||||
};
|
||||
}, [currentFit, setCurrentFit]);
|
||||
}, [previewFit, currentFit]);
|
||||
|
||||
return <CurrentFitContext.Provider value={contextValue}>{props.children}</CurrentFitContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -103,7 +103,7 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => {
|
||||
modules: fit.modules.map((module) => ({
|
||||
type_id: module.typeId,
|
||||
slot: module.slot,
|
||||
state: module.state,
|
||||
state: module.state === "Preview" ? "Active" : module.state,
|
||||
charge:
|
||||
module.charge === undefined
|
||||
? undefined
|
||||
|
||||
@@ -7,13 +7,15 @@ import { useEveData } from "@/providers/EveDataProvider";
|
||||
interface FitManager {
|
||||
/** Set the current fit. */
|
||||
setFit: (fit: EsfFit) => void;
|
||||
/** Remove the preview fit, reverting back to the actual fit. */
|
||||
removePreview: () => void;
|
||||
/** Create a new fit of the given ship type. */
|
||||
createNewFit: (typeId: number) => void;
|
||||
/** Set the name of the current fit. */
|
||||
setName: (name: string) => void;
|
||||
|
||||
/** Add an item (module, charge, drone) to the fit. */
|
||||
addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => void;
|
||||
addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge", preview?: boolean) => void;
|
||||
|
||||
/** Set a module in a slot. */
|
||||
setModule: (slot: EsfSlot, typeId: number) => void;
|
||||
@@ -37,6 +39,7 @@ interface FitManager {
|
||||
|
||||
const FitManagerContext = React.createContext<FitManager>({
|
||||
setFit: () => {},
|
||||
removePreview: () => {},
|
||||
createNewFit: () => {},
|
||||
setName: () => {},
|
||||
|
||||
@@ -71,11 +74,16 @@ export const FitManagerProvider = (props: FitManagerProps) => {
|
||||
const currentFit = useCurrentFit();
|
||||
const statistics = useStatistics();
|
||||
const setFit = currentFit.setFit;
|
||||
const setPreview = currentFit.setPreview;
|
||||
|
||||
const currentFitRef = React.useRef<EsfFit | null>(currentFit.currentFit);
|
||||
currentFitRef.current = currentFit.currentFit;
|
||||
|
||||
const contextValue = React.useMemo(() => {
|
||||
if (eveData === null) {
|
||||
return {
|
||||
setFit: () => {},
|
||||
removePreview: () => {},
|
||||
createNewFit: () => {},
|
||||
setName: () => {},
|
||||
|
||||
@@ -98,6 +106,9 @@ export const FitManagerProvider = (props: FitManagerProps) => {
|
||||
setFit: (fit: EsfFit) => {
|
||||
setFit(fit);
|
||||
},
|
||||
removePreview: () => {
|
||||
setPreview(null);
|
||||
},
|
||||
createNewFit: (typeId: number) => {
|
||||
setFit({
|
||||
name: "Unnamed Fit",
|
||||
@@ -119,8 +130,11 @@ export const FitManagerProvider = (props: FitManagerProps) => {
|
||||
});
|
||||
},
|
||||
|
||||
addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => {
|
||||
setFit((oldFit: EsfFit | null): EsfFit | null => {
|
||||
addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge", preview?: boolean) => {
|
||||
const setFitOrPreview = preview ? setPreview : setFit;
|
||||
setFitOrPreview((oldFit: EsfFit | null): EsfFit | null => {
|
||||
/* Previews are always based on the current fit. */
|
||||
if (preview) oldFit = currentFitRef.current;
|
||||
if (oldFit === null) return null;
|
||||
|
||||
if (slot === "Charge") {
|
||||
@@ -217,7 +231,7 @@ export const FitManagerProvider = (props: FitManagerProps) => {
|
||||
},
|
||||
typeId: typeId,
|
||||
charge: undefined,
|
||||
state: "Active",
|
||||
state: preview ? "Preview" : "Active",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -414,7 +428,7 @@ export const FitManagerProvider = (props: FitManagerProps) => {
|
||||
});
|
||||
},
|
||||
};
|
||||
}, [eveData, statistics?.slots, setFit]);
|
||||
}, [eveData, statistics?.slots, setFit, setPreview]);
|
||||
|
||||
return <FitManagerContext.Provider value={contextValue}>{props.children}</FitManagerContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -25,11 +25,11 @@ const TestStory = ({ fit }: { fit: EsfFit | null }) => {
|
||||
const currentFit = useCurrentFit();
|
||||
const statistics = useStatistics();
|
||||
|
||||
if (fit != currentFit.fit) {
|
||||
if (fit != currentFit.currentFit) {
|
||||
currentFit.setFit(fit);
|
||||
}
|
||||
|
||||
if (currentFit.fit === null) {
|
||||
if (currentFit.currentFit === null) {
|
||||
return <div>No fit selected</div>;
|
||||
}
|
||||
if (statistics === null) {
|
||||
|
||||
@@ -26,10 +26,18 @@ interface Statistics extends Calculation {
|
||||
slots: StatisticsSlots;
|
||||
}
|
||||
|
||||
const StatisticsContext = React.createContext<Statistics | null>(null);
|
||||
const StatisticsContext = React.createContext<{ statistics: Statistics | null; current: Statistics | null } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
export const useStatistics = () => {
|
||||
return React.useContext(StatisticsContext);
|
||||
const statistics = React.useContext(StatisticsContext);
|
||||
return statistics === null ? null : statistics.statistics;
|
||||
};
|
||||
|
||||
export const useCurrentStatistics = () => {
|
||||
const statistics = React.useContext(StatisticsContext);
|
||||
return statistics === null ? null : statistics.current ?? statistics.statistics;
|
||||
};
|
||||
|
||||
interface StatisticsProps {
|
||||
@@ -78,6 +86,10 @@ export const StatisticsProvider = (props: StatisticsProps) => {
|
||||
const currentCharacter = useCurrentCharacter();
|
||||
const dogmaEngine = useDogmaEngine();
|
||||
|
||||
const [lastStatistics, setLastStatistics] = React.useState<Statistics | null>(null);
|
||||
const lastStatisticsRef = React.useRef<Statistics | null>(lastStatistics);
|
||||
lastStatisticsRef.current = lastStatistics;
|
||||
|
||||
const contextValue = React.useMemo(() => {
|
||||
const fit = currentFit.fit;
|
||||
const skills = currentCharacter.character?.skills;
|
||||
@@ -101,8 +113,15 @@ export const StatisticsProvider = (props: StatisticsProps) => {
|
||||
|
||||
CalculateSlots(eveData, statistics);
|
||||
|
||||
return statistics;
|
||||
}, [eveData, dogmaEngine, currentFit.fit, currentCharacter.character?.skills]);
|
||||
if (!currentFit.isPreview) {
|
||||
setLastStatistics(statistics);
|
||||
}
|
||||
|
||||
return {
|
||||
statistics: statistics,
|
||||
current: currentFit.isPreview ? lastStatisticsRef.current : statistics,
|
||||
};
|
||||
}, [eveData, dogmaEngine, currentFit.fit, currentFit.isPreview, currentCharacter.character?.skills]);
|
||||
|
||||
return <StatisticsContext.Provider value={contextValue}>{props.children}</StatisticsContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { StatisticsProvider, useStatistics } from "./StatisticsProvider";
|
||||
export { StatisticsProvider, useStatistics, useCurrentStatistics } from "./StatisticsProvider";
|
||||
export type { StatisticsSlots, StatisticsSlotType } from "./StatisticsProvider";
|
||||
|
||||
Reference in New Issue
Block a user