feat: ability to enable/disable active drones from the DroneBay (#83)
This commit is contained in:
@@ -137,7 +137,7 @@ export const fullFits = [
|
||||
{ flag: 32, quantity: 1, type_id: 30836 },
|
||||
{ flag: 33, quantity: 1, type_id: 11578 },
|
||||
{ flag: 34, quantity: 1, type_id: 28756, charge: { type_id: 30488 } },
|
||||
{ flag: 87, quantity: 5, type_id: 2456 },
|
||||
{ flag: 87, quantity: 8, type_id: 2456 },
|
||||
{ flag: 92, quantity: 1, type_id: 31748 },
|
||||
{ flag: 93, quantity: 1, type_id: 31760 },
|
||||
{ flag: 94, quantity: 1, type_id: 31588 },
|
||||
|
||||
74
src/DroneBay/DroneBay.module.css
Normal file
74
src/DroneBay/DroneBay.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.droneBay {
|
||||
background-color: #111111;
|
||||
border: 1px solid #333333;
|
||||
color: #c5c5c5;
|
||||
font-size: 15px;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.droneBayEntry {
|
||||
display: flex;
|
||||
height: 64px;
|
||||
padding: 4px;
|
||||
}
|
||||
.droneBayEntry:hover {
|
||||
background-color: #321d1d;
|
||||
}
|
||||
|
||||
.droneBayEntry > div {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.droneBayEntry .middle {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.droneBayEntry .amount {
|
||||
line-height: 64px;
|
||||
text-align: center;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.droneBayEntry .name {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.droneBayEntry .selected {
|
||||
color: #909090;
|
||||
}
|
||||
|
||||
.droneBayEntry .close {
|
||||
color: #909090;
|
||||
cursor: pointer;
|
||||
line-height: 64px;
|
||||
}
|
||||
|
||||
.droneBayEntrySelected {
|
||||
background-color: #321d1d;
|
||||
border: 1px solid #52281c;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
margin-left: 2px;
|
||||
text-align: center;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.open {
|
||||
background-color: #7a3925;
|
||||
border: 1px solid #a54b32;
|
||||
}
|
||||
|
||||
.droneBayEntrySelected:hover {
|
||||
background-color: #d05c3b;
|
||||
border: 1px solid #ee6d47;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: #7a3925;
|
||||
border: 1px solid #a54b32;
|
||||
color: white;
|
||||
}
|
||||
49
src/DroneBay/DroneBay.stories.tsx
Normal file
49
src/DroneBay/DroneBay.stories.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from "../../.storybook/fits";
|
||||
|
||||
import { DogmaEngineProvider } from "../DogmaEngineProvider";
|
||||
import { EsiProvider } from "../EsiProvider";
|
||||
import { EveDataProvider } from "../EveDataProvider";
|
||||
import { ShipSnapshotProvider } from "../ShipSnapshotProvider";
|
||||
import { DroneBay } from "./";
|
||||
|
||||
const meta: Meta<typeof DroneBay> = {
|
||||
component: DroneBay,
|
||||
tags: ["autodocs"],
|
||||
title: "Component/DroneBay",
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof DroneBay>;
|
||||
|
||||
const useShipSnapshotProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
const [skills, setSkills] = React.useState<Record<string, number>>({});
|
||||
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<EsiProvider setSkills={setSkills}>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot} skills={skills}>
|
||||
<div style={{ width: context.args.width, height: context.args.width }}>
|
||||
<Story />
|
||||
</div>
|
||||
</ShipSnapshotProvider>
|
||||
</DogmaEngineProvider>
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
width: 730,
|
||||
},
|
||||
decorators: [useShipSnapshotProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
},
|
||||
},
|
||||
};
|
||||
129
src/DroneBay/DroneBay.tsx
Normal file
129
src/DroneBay/DroneBay.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
|
||||
import styles from "./DroneBay.module.css";
|
||||
import { CharAttribute, ShipAttribute } from "../ShipAttribute";
|
||||
import { ShipSnapshotContext, ShipSnapshotItem } from "../ShipSnapshotProvider";
|
||||
import { EveDataContext } from "../EveDataProvider";
|
||||
|
||||
const DroneBayEntrySelected = ({
|
||||
drone,
|
||||
index,
|
||||
isOpen,
|
||||
}: {
|
||||
drone: ShipSnapshotItem;
|
||||
index: number;
|
||||
isOpen: boolean;
|
||||
}) => {
|
||||
const snapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const onClick = React.useCallback(() => {
|
||||
snapshot.toggleDrones(drone.type_id, index + 1);
|
||||
}, [snapshot, drone, index]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.droneBayEntrySelected, {
|
||||
[styles.active]: drone.state === "Active",
|
||||
[styles.open]: isOpen,
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
{drone.state === "Active" && <>X</>}
|
||||
{drone.state === "Passive" && <> </>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const DroneBayEntry = ({ name, drones }: { name: string; drones: ShipSnapshotItem[] }) => {
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
const snapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const attributeDroneBandwidthUsedTotal = eveData.attributeMapping?.droneBandwidthUsedTotal || 0;
|
||||
const attributeDroneActive = eveData.attributeMapping?.droneActive || 0;
|
||||
const attributeDroneBandwidthUsed = eveData.attributeMapping?.droneBandwidthUsed || 0;
|
||||
const attributeDroneBandwidth = eveData.attributeMapping?.droneBandwidth || 0;
|
||||
const attributeMaxActiveDrones = eveData.attributeMapping?.maxActiveDrones || 0;
|
||||
|
||||
const bandwidthUsed = snapshot.hull?.attributes?.get(attributeDroneBandwidthUsedTotal)?.value ?? 0;
|
||||
const bandwidthAvailable = snapshot.hull?.attributes?.get(attributeDroneBandwidth)?.value ?? 0;
|
||||
const dronesActive = snapshot.hull?.attributes?.get(attributeDroneActive)?.value ?? 0;
|
||||
const maxDronesActive = snapshot.char?.attributes?.get(attributeMaxActiveDrones)?.value ?? 0;
|
||||
const droneBandwidth = drones[0].attributes?.get(attributeDroneBandwidthUsed)?.value ?? 0;
|
||||
const maxSelected = Math.max(0, Math.min(maxDronesActive, Math.floor(bandwidthAvailable / droneBandwidth)));
|
||||
|
||||
let maxOpen = Math.max(
|
||||
0,
|
||||
Math.min(maxDronesActive - dronesActive, Math.floor((bandwidthAvailable - bandwidthUsed) / droneBandwidth)),
|
||||
);
|
||||
let index = 0;
|
||||
|
||||
const dronesSelected = drones.slice(0, maxSelected);
|
||||
|
||||
const onRemove = React.useCallback(() => {
|
||||
snapshot.removeDrones(drones[0].type_id);
|
||||
}, [snapshot, drones]);
|
||||
|
||||
return (
|
||||
<div className={styles.droneBayEntry}>
|
||||
<div className={styles.amount}>{drones.length} x</div>
|
||||
<div>
|
||||
<img src={`https://images.evetech.net/types/${drones[0].type_id}/icon?size=64`} />
|
||||
</div>
|
||||
<div className={styles.middle}>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div className={styles.selected}>
|
||||
Selected:
|
||||
{Object.entries(dronesSelected).map(([key, drone]) => {
|
||||
const isOpen = drone.state !== "Active" && maxOpen > 0;
|
||||
if (isOpen) maxOpen--;
|
||||
|
||||
return <DroneBayEntrySelected key={key} drone={drone} index={index++} isOpen={isOpen} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.close} onClick={onRemove}>
|
||||
X
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const DroneBay = () => {
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
const snapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
const [drones, setDrones] = React.useState<Record<string, ShipSnapshotItem[]>>({});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (snapshot === undefined || !snapshot.loaded || snapshot.items === undefined) return;
|
||||
if (eveData === undefined || !eveData.loaded || eveData.typeIDs === undefined) return;
|
||||
|
||||
/* Group drones by type_id */
|
||||
const dronesGrouped: Record<string, ShipSnapshotItem[]> = {};
|
||||
for (const drone of snapshot.items.filter((item) => item.flag == 87)) {
|
||||
const name = eveData.typeIDs?.[drone.type_id].name ?? "";
|
||||
|
||||
if (dronesGrouped[name] === undefined) {
|
||||
dronesGrouped[name] = [];
|
||||
}
|
||||
dronesGrouped[name].push(drone);
|
||||
}
|
||||
|
||||
setDrones(dronesGrouped);
|
||||
}, [snapshot, eveData]);
|
||||
|
||||
return (
|
||||
<div className={styles.droneBay}>
|
||||
<div>
|
||||
Active drones: <ShipAttribute name="droneActive" fixed={0} /> /{" "}
|
||||
<CharAttribute name="maxActiveDrones" fixed={0} />
|
||||
</div>
|
||||
{Object.entries(drones)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([name, droneList]) => {
|
||||
return <DroneBayEntry key={name} name={name} drones={droneList} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
src/DroneBay/index.ts
Normal file
1
src/DroneBay/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DroneBay } from "./DroneBay";
|
||||
@@ -47,3 +47,20 @@
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.droneBay {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
}
|
||||
.droneBayOverlay {
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 40px;
|
||||
left: 10px;
|
||||
width: 350px;
|
||||
z-index: 101;
|
||||
}
|
||||
.droneBayVisible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import { ShipAttribute } from "../ShipAttribute";
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
import styles from "./ShipFitExtended.module.css";
|
||||
import clsx from "clsx";
|
||||
import { DroneBay } from "../DroneBay";
|
||||
|
||||
const CargoHold = () => {
|
||||
const ShipCargoHold = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.cargoIcon}>
|
||||
@@ -24,22 +26,29 @@ const CargoHold = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const DroneBay = () => {
|
||||
const ShipDroneBay = () => {
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="drone-bay" size={32} />
|
||||
</div>
|
||||
<div className={styles.cargoText}>
|
||||
<div>
|
||||
<ShipAttribute name="droneCapacityUsed" fixed={1} />
|
||||
<>
|
||||
<div onClick={() => setIsOpen(!isOpen)} className={styles.droneBay}>
|
||||
<div className={styles.cargoIcon}>
|
||||
<Icon name="drone-bay" size={32} />
|
||||
</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="droneCapacity" fixed={1} />
|
||||
<div className={styles.cargoText}>
|
||||
<div>
|
||||
<ShipAttribute name="droneCapacityUsed" fixed={1} />
|
||||
</div>
|
||||
<div>
|
||||
/ <ShipAttribute name="droneCapacity" fixed={1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>m3</div>
|
||||
</div>
|
||||
<div className={styles.cargoPostfix}>m3</div>
|
||||
</div>
|
||||
<div className={clsx(styles.droneBayOverlay, { [styles.droneBayVisible]: isOpen })}>
|
||||
<DroneBay />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -80,8 +89,8 @@ export const ShipFitExtended = () => {
|
||||
</div>
|
||||
|
||||
<div className={styles.cargoHold}>
|
||||
<CargoHold />
|
||||
<DroneBay />
|
||||
<ShipCargoHold />
|
||||
<ShipDroneBay />
|
||||
</div>
|
||||
|
||||
<div className={styles.cpuPg}>
|
||||
|
||||
@@ -71,6 +71,8 @@ interface ShipSnapshot {
|
||||
removeModule: (flag: number) => void;
|
||||
addCharge: (chargeTypeId: number) => void;
|
||||
removeCharge: (flag: number) => void;
|
||||
toggleDrones: (typeId: number, active: number) => void;
|
||||
removeDrones: (typeId: number) => void;
|
||||
changeHull: (typeId: number) => void;
|
||||
changeFit: (fit: EsiFit) => void;
|
||||
setItemState: (flag: number, state: string) => void;
|
||||
@@ -90,6 +92,8 @@ export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
|
||||
removeModule: () => {},
|
||||
addCharge: () => {},
|
||||
removeCharge: () => {},
|
||||
toggleDrones: () => {},
|
||||
removeDrones: () => {},
|
||||
changeHull: () => {},
|
||||
changeFit: () => {},
|
||||
setItemState: () => {},
|
||||
@@ -131,6 +135,8 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
removeModule: () => {},
|
||||
addCharge: () => {},
|
||||
removeCharge: () => {},
|
||||
toggleDrones: () => {},
|
||||
removeDrones: () => {},
|
||||
changeHull: () => {},
|
||||
changeFit: () => {},
|
||||
setItemState: () => {},
|
||||
@@ -294,6 +300,68 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const toggleDrones = React.useCallback((typeId: number, active: number) => {
|
||||
setCurrentFit((oldFit: EsiFit | undefined) => {
|
||||
if (oldFit === undefined) return undefined;
|
||||
|
||||
/* Find the amount of drones in the current fit. */
|
||||
const count = oldFit.items
|
||||
.filter((item) => item.flag === 87 && item.type_id === typeId)
|
||||
.reduce((acc, item) => acc + item.quantity, 0);
|
||||
if (count === 0) return oldFit;
|
||||
|
||||
/* If we request the same amount of active than we had, assume we want to deactive the current. */
|
||||
const currentActive = oldFit.items
|
||||
.filter((item) => item.flag === 87 && item.type_id === typeId && item.state === "Active")
|
||||
.reduce((acc, item) => acc + item.quantity, 0);
|
||||
if (currentActive === active) {
|
||||
active = active - 1;
|
||||
}
|
||||
|
||||
/* Ensure we never have more active than available. */
|
||||
active = Math.min(count, active);
|
||||
|
||||
/* Remove all drones of this type. */
|
||||
const newItems = oldFit.items.filter((item) => item.flag !== 87 || item.type_id !== typeId);
|
||||
|
||||
/* Add the active drones. */
|
||||
if (active > 0) {
|
||||
newItems.push({
|
||||
flag: 87,
|
||||
type_id: typeId,
|
||||
quantity: active,
|
||||
state: "Active",
|
||||
});
|
||||
}
|
||||
|
||||
/* Add the passive drones. */
|
||||
if (active < count) {
|
||||
newItems.push({
|
||||
flag: 87,
|
||||
type_id: typeId,
|
||||
quantity: count - active,
|
||||
state: "Passive",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...oldFit,
|
||||
items: newItems,
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const removeDrones = React.useCallback((typeId: number) => {
|
||||
setCurrentFit((oldFit: EsiFit | undefined) => {
|
||||
if (oldFit === undefined) return undefined;
|
||||
|
||||
return {
|
||||
...oldFit,
|
||||
items: oldFit.items.filter((item) => item.flag !== 87 || item.type_id !== typeId),
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
const changeHull = React.useCallback(
|
||||
(typeId: number) => {
|
||||
const hullName = eveData?.typeIDs?.[typeId].name;
|
||||
@@ -315,12 +383,14 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
|
||||
removeModule,
|
||||
addCharge,
|
||||
removeCharge,
|
||||
toggleDrones,
|
||||
removeDrones,
|
||||
changeHull,
|
||||
changeFit: setCurrentFit,
|
||||
setItemState,
|
||||
setName,
|
||||
}));
|
||||
}, [addModule, removeModule, addCharge, removeCharge, changeHull, setItemState, setName]);
|
||||
}, [addModule, removeModule, addCharge, removeCharge, toggleDrones, removeDrones, changeHull, setItemState, setName]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!dogmaEngine.loaded) return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./CalculationDetail";
|
||||
export * from "./DogmaEngineProvider";
|
||||
export * from "./DroneBay";
|
||||
export * from "./EsiCharacterSelection";
|
||||
export * from "./EsiProvider";
|
||||
export * from "./EveDataProvider";
|
||||
|
||||
Reference in New Issue
Block a user