feat: ability to remove modules from a fit (via hover on module) (#42)

This commit is contained in:
Patric Stout
2023-12-10 17:09:54 +01:00
committed by GitHub
parent aa76928b5f
commit fc97ab72f5
3 changed files with 130 additions and 42 deletions

View File

@@ -60,11 +60,11 @@
.ringTopItem > div, .ringTopItem > svg {
--reverse-rotation: calc(-1 * var(--rotation));
left: 50%;
pointer-events: all;
position: absolute;
top: 3.5%;
transform: rotate(var(--reverse-rotation));
}
.radialMenu {
@@ -74,18 +74,23 @@
width: 2.5%;
}
.slot {
height: 9.5%;
.slotOuter {
height: 18%;
margin-left: -2.5%;
position: absolute;
width: 7%;
}
.slot {
height: 50%;
position: relative;
user-select: none;
width: 7%;
width: 100%;
}
.slot > svg {
height: 100%;
position: absolute;
transform: rotate(var(--rotation));
width: 100%;
z-index: 4;
}
@@ -93,14 +98,15 @@
.slotImage {
height: 100%;
position: absolute;
margin-top: 8%;
margin-left: -10%;
transform: rotate(var(--reverse-rotation));
width: 100%;
z-index: 5;
}
.slotImage > img {
border-top-left-radius: 50%;
margin-left: -10%;
margin-top: 5%;
width: 120%;
}
@@ -116,9 +122,31 @@
fill: #fd2d2d;
stroke: #fd2d2d;
}
.slot[data-state="Offline"] {
.slot[data-state="Offline"] > svg, .slot[data-state="Offline"] > .slotImage {
opacity: 0.3;
}
.slot[data-state="Unavailable"] {
.slot[data-state="Unavailable"] > svg, .slot[data-state="Unavailable"] > .slotImage {
opacity: 0.1;
}
.slotOuter .slotOptions {
display: none;
}
.slotOuter[data-hasitem=true]:hover .slotOptions {
display: block;
}
.slotOptions {
position: absolute;
text-align: center;
top: 50%;
width: 100%;
}
.slotOptions > svg {
cursor: pointer;
display: block;
filter: drop-shadow(0px 2px 1px #222222);
margin: 6px auto;
transform: rotate(var(--reverse-rotation));
stroke: #ffffff;
}

View File

@@ -43,45 +43,56 @@ export const Slot = (props: { type: string, index: number, fittable: boolean, ma
let svg = <></>;
if (props.main !== undefined) {
svg = <svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin slice" style={{ display: "none" }}>
<g id="slot">
<path style={{ fillOpacity: 0.1, strokeWidth: 1, strokeOpacity: 0.5 }} d="M 243 46 A 210 210 0 0 1 279 46.7 L 276 84.7 A 172 172 0 0 0 246 84 L 243 46" />
</g>
<g id="slot-active">
<path style={{ fillOpacity: 0.6, strokeWidth: 1 }} d="M 250 84 L 254 79 L 268 79 L 272 84" />
</g>
<g id="slot-passive">
<path style={{ strokeWidth: 1 }} d="M 245 48 A 208 208 0 0 1 250 47.5 L 248 50 L 245 50" />
<path style={{ strokeWidth: 1 }} d="M 277.5 48.5 A 208 208 0 0 0 273 48 L 275 50.5 L 277.5 50.5" />
<path style={{ strokeWidth: 1 }} d="M 247 82 A 170 170 0 0 1 252 82 L 250 80 L 246.8 80" />
<path style={{ strokeWidth: 1 }} d="M 275 82.5 A 170 170 0 0 0 270 82 L 272 80 L 275.2 80" />
</g>
</svg>;
svg = <>
<svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMin slice" style={{ display: "none" }}>
<g id="slot">
<path style={{ fillOpacity: 0.1, strokeWidth: 1, strokeOpacity: 0.5 }} d="M 243 46 A 210 210 0 0 1 279 46.7 L 276 84.7 A 172 172 0 0 0 246 84 L 243 46" />
</g>
<g id="slot-active">
<path style={{ fillOpacity: 0.6, strokeWidth: 1 }} d="M 250 84 L 254 79 L 268 79 L 272 84" />
</g>
<g id="slot-passive">
<path style={{ strokeWidth: 1 }} d="M 245 48 A 208 208 0 0 1 250 47.5 L 248 50 L 245 50" />
<path style={{ strokeWidth: 1 }} d="M 277.5 48.5 A 208 208 0 0 0 273 48 L 275 50.5 L 277.5 50.5" />
<path style={{ strokeWidth: 1 }} d="M 247 82 A 170 170 0 0 1 252 82 L 250 80 L 246.8 80" />
<path style={{ strokeWidth: 1 }} d="M 275 82.5 A 170 170 0 0 0 270 82 L 272 80 L 275.2 80" />
</g>
</svg>
<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" style={{ display: "none" }}>
<g id="unfit">
<path style={{ fill: "none", strokeWidth: 1 }} d="M 4 6 A 8 8 0 1 1 4 14" />
<path style={{ fill: "none", strokeWidth: 1 }} d="M 11 6 L 6 10 L 11 14" />
<path style={{ fill: "none", strokeWidth: 1 }} d="M 6 10 L 16 10" />
</g>
<g id="offline">
<path style={{ fill: "none", strokeWidth: 1 }} d="M 12 4 A 8 8 0 1 1 6 4" />
<path style={{ fill: "none", strokeWidth: 1 }} d="M 9 2 L 9 12" />
</g>
</svg>
</>;
}
svg = <>
{svg}
<svg viewBox="235 40 52 50" xmlns="http://www.w3.org/2000/svg" className={styles.ringInner} preserveAspectRatio="xMidYMin slice">
<use href="#slot" />
{props.fittable && active && <use href="#slot-active" />}
{props.fittable && !active && <use href="#slot-passive" />}
{props.fittable && esiItem && active && <use href="#slot-active" />}
{props.fittable && esiItem && !active && <use href="#slot-passive" />}
</svg>
</>;
/* Not fittable and nothing fitted; no need to render the slot. */
if (esiItem === undefined && !props.fittable) {
return <div className={styles.slot} data-state="Unavailable">
{svg}
</div>
}
const offlineState = React.useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
e.stopPropagation();
if (!shipSnapshot?.loaded || !esiItem) return;
if (esiItem !== undefined) {
item = <img src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`} title={eveData?.typeIDs?.[esiItem.type_id].name} />
}
if (esiItem.state === "Passive") {
shipSnapshot.setItemState(esiItem.flag, "Online");
} else {
shipSnapshot.setItemState(esiItem.flag, "Passive");
}
}, [shipSnapshot, esiItem]);
const state = (esiItem?.state === "Passive" && esiItem?.max_state !== "Passive") ? "Offline" : esiItem?.state;
function cycleState(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
const cycleState = React.useCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (!shipSnapshot?.loaded || !esiItem) return;
const states = stateRotation[esiItem.max_state];
@@ -95,12 +106,46 @@ export const Slot = (props: { type: string, index: number, fittable: boolean, ma
}
shipSnapshot.setItemState(esiItem.flag, newState);
}, [shipSnapshot, esiItem]);
const unfitModule = React.useCallback((e: React.MouseEvent<SVGSVGElement, MouseEvent>) => {
e.stopPropagation();
if (!shipSnapshot?.loaded || !esiItem) return;
shipSnapshot.removeModule(esiItem.flag);
}, [shipSnapshot, esiItem]);
/* Not fittable and nothing fitted; no need to render the slot. */
if (esiItem === undefined && !props.fittable) {
return <div className={styles.slotOuter} data-hasitem={false}>
<div className={styles.slot} data-state="Unavailable">
{svg}
</div>
</div>
}
return <div className={styles.slot} onClick={cycleState} data-state={state}>
{svg}
<div className={styles.slotImage}>
{item}
if (esiItem !== undefined) {
item = <img src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`} title={eveData?.typeIDs?.[esiItem.type_id].name} />
}
const state = (esiItem?.state === "Passive" && esiItem?.max_state !== "Passive") ? "Offline" : esiItem?.state;
return <div className={styles.slotOuter} data-hasitem={esiItem !== undefined}>
<div className={styles.slot} onClick={cycleState} data-state={state}>
{svg}
<div className={styles.slotImage}>
{item}
</div>
</div>
<div className={styles.slotOptions}>
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={unfitModule}>
<use href="#unfit" />
</svg>
{esiItem?.max_state !== "Passive" &&
<svg viewBox="0 0 20 20" width={20} xmlns="http://www.w3.org/2000/svg" onClick={offlineState}>
<use href="#offline" />
</svg>
}
</div>
</div>
}

View File

@@ -58,6 +58,7 @@ interface ShipSnapshot {
fit?: EsiFit;
addModule: (typeId: number, slot: ShipSnapshotSlotsType | "dronebay") => void;
removeModule: (flag: number) => void;
changeHull: (typeId: number) => void;
changeFit: (fit: EsiFit) => void;
setItemState: (flag: number, state: string) => void;
@@ -73,6 +74,7 @@ export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
"rig": 0,
},
addModule: () => {},
removeModule: () => {},
changeHull: () => {},
changeFit: () => {},
setItemState: () => {},
@@ -110,6 +112,7 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
"rig": 0,
},
addModule: () => {},
removeModule: () => {},
changeHull: () => {},
changeFit: () => {},
setItemState: () => {},
@@ -172,6 +175,17 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
})
}, [shipSnapshot.slots]);
const removeModule = React.useCallback((flag: number) => {
setCurrentFit((oldFit: EsiFit | undefined) => {
if (oldFit === undefined) return undefined;
return {
...oldFit,
items: oldFit.items.filter((item) => item.flag !== flag),
};
})
}, []);
const changeHull = React.useCallback((typeId: number) => {
setCurrentFit({
"name": "New Ship",
@@ -185,11 +199,12 @@ export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
setShipSnapshot((oldSnapshot) => ({
...oldSnapshot,
addModule,
removeModule,
changeHull,
changeFit: setCurrentFit,
setItemState,
}));
}, [addModule, changeHull, setItemState]);
}, [addModule, removeModule, changeHull, setItemState]);
React.useEffect(() => {
if (!dogmaEngine.loaded) return;