mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-07-19 17:12:58 -05:00
Merge pull request #979 from curtgrimes/draggable-abilities
Add ability to drag abilities into slots
This commit is contained in:
commit
e1b930ced2
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)} />
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export function Image({
|
|||
width={width}
|
||||
height={height}
|
||||
style={style}
|
||||
draggable="false"
|
||||
/>
|
||||
</picture>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user