feat: ability to enable/disable active drones from the DroneBay (#83)

This commit is contained in:
Patric Stout
2024-05-09 16:07:54 +02:00
committed by GitHub
parent 1b9c08dfbf
commit e3caad1a32
9 changed files with 367 additions and 17 deletions

View File

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

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

View 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
View 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" && <>&nbsp;</>}
</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
View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
export * from "./CalculationDetail";
export * from "./DogmaEngineProvider";
export * from "./DroneBay";
export * from "./EsiCharacterSelection";
export * from "./EsiProvider";
export * from "./EveDataProvider";