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:
Patric Stout
2024-05-26 14:53:31 +02:00
committed by GitHub
parent d8bb534526
commit 72bbadeaee
26 changed files with 260 additions and 102 deletions

View File

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

View File

@@ -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 &quot;{currentFit.fit?.name}&quot;; do you want to update it?
You have a local fitting with the name &quot;{currentFit.currentFit?.name}&quot;; do you want to update it?
</div>
<div className={styles.alreadyExistsButtons}>
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => saveBrowser(true)}>

View File

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

View File

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

View File

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

View File

@@ -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]);

View File

@@ -0,0 +1,7 @@
.increase {
color: #8dc169;
}
.decrease {
color: #ff454b;
}

View File

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

View File

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

View File

@@ -209,3 +209,7 @@
transform: rotate(var(--reverse-rotation));
stroke: #ffffff;
}
.preview {
filter: sepia(100%) hue-rotate(190deg) saturate(200%);
}

View File

@@ -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" })}
/>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,2 +1,2 @@
export { StatisticsProvider, useStatistics } from "./StatisticsProvider";
export { StatisticsProvider, useStatistics, useCurrentStatistics } from "./StatisticsProvider";
export type { StatisticsSlots, StatisticsSlotType } from "./StatisticsProvider";