Merge pull request #979 from curtgrimes/draggable-abilities

Add ability to drag abilities into slots
This commit is contained in:
Kalle 2022-10-02 14:11:28 +03:00 committed by GitHub
commit e1b930ced2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 155 additions and 26 deletions

View File

@ -1,3 +1,5 @@
import clsx from "clsx";
import React from "react";
import invariant from "tiny-invariant";
import { abilities } from "~/modules/in-game-lists";
import type { BuildAbilitiesTupleWithUnknown } from "~/modules/in-game-lists/types";
@ -40,6 +42,38 @@ export function AbilitiesSelector({
onChange(addAbility({ oldAbilities: selectedAbilities, ability }));
};
const [draggingAbility, setDraggingAbility] = React.useState<
typeof abilities[number] | undefined
>();
const onDragStart =
(ability: typeof abilities[number]) => (event: React.DragEvent) => {
setDraggingAbility(ability);
event.dataTransfer.setData("text/plain", JSON.stringify(ability));
};
const onDragEnd = () => {
setDraggingAbility(undefined);
};
const onDrop =
(atRowIndex: number, atAbilityIndex: number) =>
(event: React.DragEvent) => {
event.preventDefault();
const ability = JSON.parse(
event.dataTransfer.getData("text/plain")
) as typeof abilities[number];
onChange(
addAbility({
oldAbilities: selectedAbilities,
ability,
atRowIndex,
atAbilityIndex,
})
);
};
return (
<div className="ability-selector__container">
<div className="ability-selector__slots">
@ -50,6 +84,13 @@ export function AbilitiesSelector({
ability={ability}
size={abilityI === 0 ? "MAIN" : "SUB"}
onClick={() => onSlotClick({ rowI, abilityI })}
dragStarted={!!draggingAbility}
dropAllowed={canPlaceAbilityAtSlot(
rowI,
abilityI,
draggingAbility
)}
onDrop={onDrop(rowI, abilityI)}
/>
))
)}
@ -58,10 +99,15 @@ export function AbilitiesSelector({
{abilities.map((ability) => (
<button
key={ability.name}
className="ability-selector__ability-button"
className={clsx("ability-selector__ability-button", {
"is-dragging": ability.name === draggingAbility?.name,
})}
type="button"
onClick={() => onButtonClick(ability)}
data-cy={`${ability.name}-ability-button`}
draggable="true"
onDragStart={onDragStart(ability)}
onDragEnd={onDragEnd}
>
<Image
alt=""
@ -76,45 +122,81 @@ export function AbilitiesSelector({
);
}
const canPlaceAbilityAtSlot = (
rowIndex: number,
abilityIndex: number,
ability?: typeof abilities[number]
) => {
if (!ability) {
return false;
}
const legalGearTypeForMain =
rowIndex === 0
? "HEAD_MAIN_ONLY"
: rowIndex === 1
? "CLOTHES_MAIN_ONLY"
: "SHOES_MAIN_ONLY";
const isMainSlot = abilityIndex === 0;
if (
!["STACKABLE", legalGearTypeForMain].includes(ability.type) &&
isMainSlot
) {
// Can't put this type of gear in main slot
return false;
}
if (!isMainSlot && ability.type !== "STACKABLE") {
// Can't put main slot only gear to sub slots
return false;
}
return true;
};
function addAbility({
oldAbilities,
ability,
atRowIndex,
atAbilityIndex,
}: {
oldAbilities: BuildAbilitiesTupleWithUnknown;
ability: typeof abilities[number];
atRowIndex?: number;
atAbilityIndex?: number;
}): BuildAbilitiesTupleWithUnknown {
const abilitiesClone = JSON.parse(
JSON.stringify(oldAbilities)
) as BuildAbilitiesTupleWithUnknown;
for (const [i, row] of abilitiesClone.entries()) {
const legalGearTypeForMain =
i === 0
? "HEAD_MAIN_ONLY"
: i === 1
? "CLOTHES_MAIN_ONLY"
: "SHOES_MAIN_ONLY";
if (atRowIndex !== undefined && atAbilityIndex !== undefined) {
// Attempt to place the ability at a specific slot since we
// were given an atRowIndex and atAbilityIndex
if (canPlaceAbilityAtSlot(atRowIndex, atAbilityIndex, ability)) {
// Assign this ability to the slot
abilitiesClone[atRowIndex]![atAbilityIndex] = ability.name;
}
} else {
// Loop through all slots and attempt to place this ability
// in the first empty one
for (const [rowIndex, row] of abilitiesClone.entries()) {
for (const [abilityIndex, oldAbility] of row.entries()) {
if (oldAbility !== "UNKNOWN") {
// Skip any filled slots in this loop until we arrive at an empty one.
continue;
}
for (const [j, oldAbility] of row.entries()) {
const isMainSlot = j === 0;
if (!canPlaceAbilityAtSlot(rowIndex, abilityIndex, ability)) {
// This ability isn't valid for this slot
continue;
}
// slot is not empty
if (oldAbility !== "UNKNOWN") continue;
// Assign this ability to the slot
abilitiesClone[rowIndex]![abilityIndex] = ability.name;
// can't put this type of gear in main slot
if (
!["STACKABLE", legalGearTypeForMain].includes(ability.type) &&
isMainSlot
) {
continue;
return abilitiesClone;
}
// can't put main slot only gear to sub slots
if (!isMainSlot && ability.type !== "STACKABLE") continue;
abilitiesClone[i]![j] = ability.name;
return abilitiesClone;
}
}

View File

@ -1,3 +1,5 @@
import clsx from "clsx";
import React from "react";
import type { AbilityWithUnknown } from "~/modules/in-game-lists/types";
import { abilityImageUrl } from "~/utils/urls";
import { Image } from "./Image";
@ -11,17 +13,38 @@ const sizeMap = {
export function Ability({
ability,
size,
dragStarted = false,
dropAllowed = false,
onClick,
onDrop,
}: {
ability: AbilityWithUnknown;
size: keyof typeof sizeMap;
dragStarted?: boolean;
dropAllowed?: boolean;
onClick?: () => void;
onDrop?: (event: React.DragEvent) => void;
}) {
const sizeNumber = sizeMap[size];
const [isDragTarget, setIsDragTarget] = React.useState(false);
const onDragOver = (event: React.DragEvent) => {
event.preventDefault();
setIsDragTarget(true);
};
const onDragLeave = () => {
setIsDragTarget(false);
};
return (
<button
className="build__ability"
className={clsx("build__ability", {
"is-drag-target": isDragTarget,
"drag-started": dragStarted,
"drop-allowed": dropAllowed,
})}
style={
{
"--ability-size": `${sizeNumber}px`,
@ -29,6 +52,12 @@ export function Ability({
}
onClick={onClick}
data-cy={`${ability}-ability`}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={(event) => {
setIsDragTarget(false);
onDrop?.(event);
}}
type="button"
>
<Image alt="" path={abilityImageUrl(ability)} />

View File

@ -32,6 +32,7 @@ export function Image({
width={width}
height={height}
style={style}
draggable="false"
/>
</picture>
);

View File

@ -792,9 +792,22 @@ dialog::backdrop {
background-size: 100%;
border-radius: 50%;
box-shadow: 0 0 0 1px var(--bg-ability);
transform: scale(1);
transition: all 0.1s ease;
user-select: none;
}
.build__ability.is-drag-target {
background: var(--abilities-button-bg);
transform: scale(1.15);
}
.build__ability.drag-started:not(.drop-allowed) {
filter: grayscale(1);
opacity: 0.3;
pointer-events: none;
}
.build__bottom-row {
display: flex;
height: 100%;
@ -841,3 +854,7 @@ dialog::backdrop {
background-color: var(--abilities-button-bg);
border-radius: 50%;
}
.ability-selector__ability-button.is-dragging {
box-shadow: 0 0 100px inset rgb(255 255 255 / 25%);
}