Skip to content

Commit

Permalink
feat: import/show/change cargobay (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
TrueBrain authored Jun 2, 2024
1 parent d67b489 commit 7edb3b5
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 23 deletions.
10 changes: 10 additions & 0 deletions .storybook/fits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ Loki Defensive - Covert Reconfiguration
Loki Offensive - Launcher Efficiency Configuration
Loki Propulsion - Wake Limiter
Hammerhead II x1
Covert Ops Cloaking Device II x2
Nova Javelin Heavy Assault Missile x9000
Nova Rage Heavy Assault Missile x9000
Sisters Combat Scanner Probe x16
Scourge Rage Heavy Assault Missile x330
Sisters Core Scanner Probe x16
Nanite Repair Paste x150
'Packrat' Mobile Tractor Unit x1
`,
};

Expand Down
42 changes: 42 additions & 0 deletions src/components/CargoBay/CargoBay.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
.cargoBay {
background-color: #111111;
border: 1px solid #333333;
color: #c5c5c5;
font-size: 15px;
padding: 5px;
position: relative;
}

.cargoBayEntry {
display: flex;
height: 32px;
padding: 4px;
}
.cargoBayEntry:hover {
background-color: #321d1d;
}

.cargoBayEntry > div {
margin-right: 8px;
}

.cargoBayEntry .middle {
flex-grow: 1;
}

.cargoBayEntry .amount {
line-height: 32px;
text-align: right;
width: 50px;
}

.cargoBayEntry .name {
line-height: 32px;
text-align: center;
}

.cargoBayEntry .close {
color: #909090;
cursor: pointer;
line-height: 32px;
}
39 changes: 39 additions & 0 deletions src/components/CargoBay/CargoBay.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";

import { fitArgType } from "../../../.storybook/fits";
import { useFitSelection, withDecoratorFull } from "../../../.storybook/helpers";

import { EsfFit } from "@/providers/CurrentFitProvider";

import { CargoBay } from "./";

type StoryProps = React.ComponentProps<typeof CargoBay> & { fit: EsfFit | null; width: number };

const meta: Meta<StoryProps> = {
component: CargoBay,
tags: ["autodocs"],
};

export default meta;
type Story = StoryObj<StoryProps>;

export const Default: Story = {
argTypes: {
fit: fitArgType,
},
args: {
fit: null,
width: 730,
},
decorators: [withDecoratorFull],
render: ({ fit, width, ...args }) => {
useFitSelection(fit);

return (
<div style={{ width: width, height: width }}>
<CargoBay {...args} />
</div>
);
},
};
99 changes: 99 additions & 0 deletions src/components/CargoBay/CargoBay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";

import { useFitManager } from "@/providers/FitManagerProvider";
import { useEveData } from "@/providers/EveDataProvider";
import { EsfCargo, useCurrentFit } from "@/providers/CurrentFitProvider";
import { CalculationSlotType } from "@/providers/DogmaEngineProvider";

import styles from "./CargoBay.module.css";

const OnItemDragStart = (
typeId: number,
slotType?: CalculationSlotType,
): ((e: React.DragEvent<HTMLDivElement>) => void) => {
return (e: React.DragEvent<HTMLDivElement>) => {
const img = new Image();
img.src = `https://images.evetech.net/types/${typeId}/icon?size=64`;
e.dataTransfer.setDragImage(img, 32, 32);
e.dataTransfer.setData("application/esf-type-id", typeId.toString());
if (slotType !== undefined) {
e.dataTransfer.setData("application/esf-slot-type", slotType);
}
};
};

const CargoBayEntry = ({ name, cargo }: { name: string; cargo: EsfCargo }) => {
const eveData = useEveData();
const fitManager = useFitManager();

const onRemove = React.useCallback(() => {
fitManager.removeCargo(cargo.typeId);
}, [fitManager, cargo]);

let slotType: CalculationSlotType | undefined = undefined;
if (eveData?.typeIDs[cargo.typeId]?.categoryID === 18) {
slotType = "DroneBay";
} else if (eveData?.typeIDs[cargo.typeId]?.categoryID === 8) {
slotType = "Charge";
} else {
slotType = eveData?.typeDogma[cargo.typeId]?.dogmaEffects
.map((effect) => {
switch (effect.effectID) {
case eveData.effectMapping.loPower:
return "Low";
case eveData.effectMapping.medPower:
return "Medium";
case eveData.effectMapping.hiPower:
return "High";
case eveData.effectMapping.rigSlot:
return "Rig";
case eveData.effectMapping.subSystem:
return "SubSystem";
}
})
.filter((slot) => slot !== undefined)[0];
}

return (
<div className={styles.cargoBayEntry} onDragStart={OnItemDragStart(cargo.typeId, slotType)} draggable>
<div className={styles.amount}>{cargo.quantity} x</div>
<div>
<img src={`https://images.evetech.net/types/${cargo.typeId}/icon?size=32`} />
</div>
<div className={styles.middle}>
<div className={styles.name}>{name}</div>
</div>
<div className={styles.close} onClick={onRemove}>
X
</div>
</div>
);
};

export const CargoBay = () => {
const eveData = useEveData();
const currentFit = useCurrentFit();

if (eveData === null || currentFit?.fit === null) return <></>;

/* Fetch name of all cargo items. */
const cargoList: { name: string; item: EsfCargo }[] = [];
for (const item of currentFit.fit.cargo) {
const name = eveData.typeIDs?.[item.typeId].name ?? "";

cargoList.push({
name,
item,
});
}

return (
<div className={styles.cargoBay}>
{Object.values(cargoList)
.sort((a, b) => a.name.localeCompare(b.name))
.map(({ name, item }) => {
return <CargoBayEntry key={name} name={name} cargo={item} />;
})}
</div>
);
};
1 change: 1 addition & 0 deletions src/components/CargoBay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { CargoBay } from "./CargoBay";
9 changes: 7 additions & 2 deletions src/components/ShipAttribute/ShipAttribute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export interface AttributeProps {
divideBy?: number;
/** Whether to forcefully round down. */
roundDown?: boolean;
/** Whether to forcefully round up. */
roundUp?: boolean;
}

export enum AttributeChange {
Expand All @@ -44,7 +46,10 @@ export function useAttribute(type: "Ship" | "Char", props: AttributeProps): { va
value = 0;
currentValue = 0;
} else {
if (type === "Ship") {
if (type === "Ship" && props.name === "capacityUsed") {
value = statistics.capacityUsed;
currentValue = currentStatistics?.capacityUsed;
} else if (type === "Ship") {
value = statistics.hull.attributes.get(attributeId)?.value;
currentValue = currentStatistics?.hull.attributes.get(attributeId)?.value;
} else {
Expand Down Expand Up @@ -82,7 +87,7 @@ export function useAttribute(type: "Ship" | "Char", props: AttributeProps): { va

const k = Math.pow(10, props.fixed);
if (k > 0) {
if (props.isResistance) {
if (props.isResistance || props.roundUp) {
value -= 1 / k / 10;
value = Math.ceil(value * k) / k;
} else if (props.roundDown) {
Expand Down
15 changes: 11 additions & 4 deletions src/components/ShipFitExtended/ShipFitExtended.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,29 @@
margin-left: 5px;
}

.droneBay {
.droneBay,
.cargoBay {
cursor: pointer;
position: relative;
user-select: none;
z-index: 100;
}
.droneBayOverlay {
.droneBayOverlay,
.cargoBayOverlay {
display: none;
position: absolute;
bottom: 40px;
left: 10px;
width: 350px;
width: 450px;
z-index: 101;
}
.droneBayVisible {
.droneBayVisible,
.cargoBayVisible {
display: block;
}
.cargoBayOverlay {
bottom: 80px;
}

.empty {
align-items: center;
Expand Down
76 changes: 65 additions & 11 deletions src/components/ShipFitExtended/ShipFitExtended.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,77 @@
import clsx from "clsx";
import React from "react";

import { CargoBay } from "@/components/CargoBay";
import { DroneBay } from "@/components/DroneBay";
import { Icon } from "@/components/Icon";
import { ShipAttribute } from "@/components/ShipAttribute";
import { ShipFit } from "@/components/ShipFit";
import { FitHistory } from "@/components/FitHistory";
import { useCurrentFit } from "@/providers/CurrentFitProvider";
import { useEveData } from "@/providers/EveDataProvider";
import { useStatistics } from "@/providers/StatisticsProvider";
import { useFitManager } from "@/providers/FitManagerProvider";

import styles from "./ShipFitExtended.module.css";

const ShipCargoHold = () => {
const statistics = useStatistics();
const fitManager = useFitManager();

const [isOpen, setIsOpen] = React.useState(false);

const onDragOver = React.useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
}, []);

const onDragEnd = React.useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();

const parseNumber = (maybeNumber: string): number | undefined => {
const num = parseInt(maybeNumber);
return Number.isInteger(num) ? num : undefined;
};

const draggedTypeId: number | undefined = parseNumber(e.dataTransfer.getData("application/esf-type-id"));
if (draggedTypeId === undefined) {
return;
}

fitManager.addCargo(draggedTypeId, 1);
},
[fitManager],
);

if (statistics?.capacityUsed === 0 && isOpen) {
setIsOpen(false);
}

return (
<div>
<div className={styles.cargoIcon}>
<Icon name="cargo-hold" size={32} />
</div>
<div className={styles.cargoText}>
<div>0</div>
<div>
/ <ShipAttribute name="capacity" fixed={1} />
<>
<div
onClick={() => setIsOpen(statistics?.capacityUsed !== 0 && !isOpen)}
className={clsx({ [styles.cargoBay]: statistics?.capacityUsed !== 0 })}
onDragOver={onDragOver}
onDrop={onDragEnd}
>
<div className={styles.cargoIcon}>
<Icon name="cargo-hold" size={32} />
</div>
<div className={styles.cargoText}>
<div>
<ShipAttribute name="capacityUsed" fixed={1} roundUp />
</div>
<div>
/ <ShipAttribute name="capacity" fixed={1} />
</div>
</div>
<div className={styles.cargoPostfix}>m3</div>
</div>
<div className={styles.cargoPostfix}>m3</div>
</div>
<div className={clsx(styles.cargoBayOverlay, { [styles.cargoBayVisible]: isOpen })}>
<CargoBay />
</div>
</>
);
};

Expand All @@ -38,9 +85,16 @@ const ShipDroneBay = () => {

const isStructure = eveData.typeIDs[currentFit.currentFit?.shipTypeId ?? 0]?.categoryID === 65;

if (currentFit.fit?.drones.length === 0 && isOpen) {
setIsOpen(false);
}

return (
<>
<div onClick={() => setIsOpen(!isOpen)} className={styles.droneBay}>
<div
onClick={() => setIsOpen(currentFit.fit?.drones.length !== 0 && !isOpen)}
className={clsx({ [styles.droneBay]: currentFit.fit?.drones.length !== 0 })}
>
<div className={styles.cargoIcon}>
<Icon name="drone-bay" size={32} />
</div>
Expand Down
Loading

0 comments on commit 7edb3b5

Please sign in to comment.