123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815 |
- import React from "react";
- import { useTranslation } from "react-i18next";
- import { toast } from "react-toastify";
- import { useItemActions, useUsers, useSelectedItems } from "react-sync-board";
- import {
- shuffle as shuffleArray,
- randInt,
- uid,
- getItemElement,
- playAudio,
- } from "../utils";
- import RotateActionForm from "./forms/RotateActionForm";
- import RandomlyRotateActionForm from "./forms/RandomlyRotateActionForm";
- import deleteIcon from "../media/images/delete.svg";
- import stackToCenterIcon from "../media/images/stackToCenter.svg";
- import stackToTopLeftIcon from "../media/images/stackToTopLeft.svg";
- import alignAsLineIcon from "../media/images/alignAsLine.svg";
- import alignAsSquareIcon from "../media/images/alignAsSquare.svg";
- import duplicateIcon from "../media/images/duplicate.svg";
- import seeIcon from "../media/images/see.svg";
- import flipIcon from "../media/images/flip.svg";
- import lockIcon from "../media/images/lock.svg";
- import rotateIcon from "../media/images/rotate.svg";
- import shuffleIcon from "../media/images/shuffle.svg";
- import tapIcon from "../media/images/tap.svg";
- import rollIcon from "../media/images/rolling-dices.svg";
- import emptyBagIcon from "../media/images/emptybag.svg"
- import bagExtractIcon from "../media/images/bagextract.svg"
- import shuffleBagIcon from "../media/images/shufflebag.svg"
- import flipAudio from "../media/audio/flip.wav?url";
- import rollAudio from "../media/audio/roll.wav?url";
- import shuffleAudio from "../media/audio/shuffle.wav?url";
- import useLocalStorage from "../hooks/useLocalStorage";
- export const useGameItemActions = () => {
- const {
- batchUpdateItems,
- removeItems,
- pushItem,
- pushItems,
- reverseItemsOrder,
- swapItems,
- getItems,
- } = useItemActions();
- const { t } = useTranslation();
- const [isFirstLock, setIsFirstLock] = useLocalStorage("isFirstLock", true);
- const { currentUser } = useUsers();
- const selectedItems = useSelectedItems();
- const getItemListOrSelected = React.useCallback(
- async (itemIds) => {
- if (itemIds) {
- return [itemIds, await getItems(itemIds)];
- } else {
- return [selectedItems, await getItems(selectedItems)];
- }
- },
- [getItems, selectedItems]
- );
- const isMountedRef = React.useRef(false);
- React.useEffect(() => {
- // Mounted guard
- isMountedRef.current = true;
- return () => {
- isMountedRef.current = false;
- };
- }, []);
- // Stack selection to Center
- const stackToCenter = React.useCallback(
- async (
- itemIds,
- {
- stackThicknessMin = 0.5,
- stackThicknessMax = 1,
- limitCardsNumber = 32,
- } = {}
- ) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- // Rule to manage thickness of the stack.
- let stackThickness = stackThicknessMax;
- if (items.length >= limitCardsNumber) {
- stackThickness = stackThicknessMin;
- }
- // To avoid displacement effects.
- let isSameGap = true;
- for (let i = 1; i < items.length; i++) {
- if (Math.abs(items[i].x - items[i - 1].x) != stackThickness) {
- isSameGap = false;
- break;
- }
- if (Math.abs(items[i].y - items[i - 1].y) != stackThickness) {
- isSameGap = false;
- break;
- }
- }
- if (isSameGap == true) {
- return;
- }
- // Compute middle position
- const minMax = { min: {}, max: {} };
- minMax.min.x = Math.min(...items.map(({ x }) => x));
- minMax.min.y = Math.min(...items.map(({ y }) => y));
- minMax.max.x = Math.max(
- ...items.map(({ x, id }) => x + getItemElement(id).clientWidth)
- );
- minMax.max.y = Math.max(
- ...items.map(({ y, id }) => y + getItemElement(id).clientHeight)
- );
- const { clientWidth, clientHeight } = getItemElement(items[0].id);
- let newX =
- minMax.min.x + (minMax.max.x - minMax.min.x) / 2 - clientWidth / 2;
- let newY =
- minMax.min.y + (minMax.max.y - minMax.min.y) / 2 - clientHeight / 2;
- batchUpdateItems(ids, (item) => {
- const newItem = {
- ...item,
- x: newX,
- y: newY,
- };
- newX += stackThickness;
- newY -= stackThickness;
- return newItem;
- });
- },
- [batchUpdateItems, getItemListOrSelected]
- );
- // Stack selection to Top Left
- const stackToTopLeft = React.useCallback(
- async (
- itemIds,
- {
- stackThicknessMin = 0.5,
- stackThicknessMax = 1,
- limitCardsNumber = 32,
- } = {}
- ) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- let { x: newX, y: newY } = items[0];
- // Rule to manage thickness of the stack.
- let stackThickness = stackThicknessMax;
- if (items.length >= limitCardsNumber) {
- stackThickness = stackThicknessMin;
- }
- batchUpdateItems(ids, (item) => {
- const newItem = {
- ...item,
- x: newX,
- y: newY,
- };
- newX += stackThickness;
- newY -= stackThickness;
- return newItem;
- });
- },
- [batchUpdateItems, getItemListOrSelected]
- );
- // Align selection to a line
- const alignAsLine = React.useCallback(
- async (itemIds, { gapBetweenItems = 5 } = {}) => {
- // Negative value is possible for 'gapBetweenItems'.
- const [ids, items] = await getItemListOrSelected(itemIds);
- let { x: newX, y: newY } = items[0];
- batchUpdateItems(ids, (item) => {
- const { clientWidth } = getItemElement(item.id);
- const newItem = {
- ...item,
- x: newX,
- y: newY,
- };
- newX += clientWidth + gapBetweenItems;
- return newItem;
- });
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- // Align selection to an array
- const alignAsSquare = React.useCallback(
- async (itemIds, { gapBetweenItems = 5 } = {}) => {
- // Negative value is possible for 'gapBetweenItems'.
- const [ids, items] = await getItemListOrSelected(itemIds);
- // Count number of elements
- const numberOfElements = items.length;
- const numberOfColumns = Math.ceil(Math.sqrt(numberOfElements));
- let { x: newX, y: newY } = items[0];
- let currentColumn = 1;
- batchUpdateItems(ids, (item) => {
- const { clientWidth, clientHeight } = getItemElement(item.id);
- const newItem = {
- ...item,
- x: newX,
- y: newY,
- };
- newX += clientWidth + gapBetweenItems;
- currentColumn += 1;
- if (currentColumn > numberOfColumns) {
- currentColumn = 1;
- newX = items[0].x;
- newY += clientHeight + gapBetweenItems;
- }
- return newItem;
- });
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- const snapToPoint = React.useCallback(
- async (itemIds, { x, y } = {}) => {
- batchUpdateItems(itemIds, (item) => {
- const { clientWidth, clientHeight } = getItemElement(item.id);
- let newX = x - clientWidth / 2;
- let newY = y - clientHeight / 2;
- const newItem = {
- ...item,
- x: newX,
- y: newY,
- };
- return newItem;
- });
- },
- [batchUpdateItems]
- );
- const roll = React.useCallback(
- async (itemIds) => {
- const [ids] = await getItemListOrSelected(itemIds);
- ids.forEach((itemId) => {
- const elem = getItemElement(itemId);
- elem.firstChild.className = "hvr-wobble-horizontal";
- });
- const simulateRoll = (nextTimeout) => {
- batchUpdateItems(ids, (item) => {
- return {
- ...item,
- value: randInt(0, (item.side || 6) - 1),
- };
- });
- if (nextTimeout < 300) {
- setTimeout(
- () => simulateRoll(nextTimeout + randInt(10, 50)),
- nextTimeout
- );
- }
- };
- simulateRoll(100);
- playAudio(rollAudio, 0.4);
- },
- [batchUpdateItems, getItemListOrSelected]
- );
- const shuffleItems = React.useCallback(
- async (itemIds) => {
- const [ids] = await getItemListOrSelected(itemIds);
- ids.forEach((itemId) => {
- const elem = getItemElement(itemId);
- elem.firstChild.className = "hvr-wobble-horizontal";
- });
- const shuffledItems = shuffleArray([...ids]);
- swapItems(ids, shuffledItems);
- playAudio(shuffleAudio, 0.5);
- },
- [getItemListOrSelected, swapItems]
- );
- const randomlyRotateSelectedItems = React.useCallback(
- async (itemIds, { angle, maxRotateCount = 0 }) => {
- const [ids] = await getItemListOrSelected(itemIds);
- const maxRotate = maxRotateCount || Math.round(360 / angle);
- batchUpdateItems(ids, (item) => {
- const rotation =
- ((item.rotation || 0) + angle * randInt(0, maxRotate)) % 360;
- return { ...item, rotation };
- });
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- // Tap/Untap elements
- const toggleTap = React.useCallback(
- async (itemIds) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- const tappedCount = items.filter(({ rotation }) => rotation === 90)
- .length;
- let untap = false;
- if (tappedCount > ids.length / 2) {
- untap = true;
- }
- batchUpdateItems(ids, (item) => ({
- ...item,
- rotation: untap ? 0 : 90,
- }));
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- // Lock / unlock elements
- const toggleLock = React.useCallback(
- async (itemIds) => {
- const [ids] = await getItemListOrSelected(itemIds);
- batchUpdateItems(ids, (item) => ({
- ...item,
- locked: !item.locked,
- }));
- // Help user on first lock
- if (isFirstLock) {
- toast.info(
- t("You've locked your first element. Long click to select it again."),
- { autoClose: false }
- );
- setIsFirstLock(false);
- }
- },
- [getItemListOrSelected, batchUpdateItems, isFirstLock, t, setIsFirstLock]
- );
- // Flip or reveal items
- const setFlip = React.useCallback(
- async (itemIds, { flip = true, reverseOrder = true } = {}) => {
- batchUpdateItems(itemIds, (item) => ({
- ...item,
- flipped: flip,
- unflippedFor:
- !Array.isArray(item.unflippedFor) || item.unflippedFor.length > 0
- ? null
- : item.unflippedFor,
- }));
- if (reverseOrder) {
- reverseItemsOrder(itemIds);
- }
- playAudio(flipAudio, 0.2);
- },
- [batchUpdateItems, reverseItemsOrder]
- );
- // Toggle flip state
- const toggleFlip = React.useCallback(
- async (itemIds, { reverseOrder = true } = {}) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- const flippedCount = items.filter(({ flipped }) => flipped).length;
- setFlip(ids, {
- flip: flippedCount < ids.length / 2,
- reverseOrder,
- });
- },
- [getItemListOrSelected, setFlip]
- );
- // Rotate element
- const rotate = React.useCallback(
- async (itemIds, { angle }) => {
- const [ids] = await getItemListOrSelected(itemIds);
- batchUpdateItems(ids, (item) => ({
- ...item,
- rotation: (item.rotation || 0) + angle,
- }));
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- // Reveal for player only
- const setFlipSelf = React.useCallback(
- async (itemIds, { flipSelf = true } = {}) => {
- batchUpdateItems(itemIds, (item) => {
- let { unflippedFor = [] } = item;
- if (!Array.isArray(item.unflippedFor)) {
- unflippedFor = [];
- }
- if (flipSelf && !unflippedFor.includes(currentUser.uid)) {
- unflippedFor = [...unflippedFor, currentUser.uid];
- }
- if (!flipSelf && unflippedFor.includes(currentUser.uid)) {
- unflippedFor = unflippedFor.filter((id) => id !== currentUser.uid);
- }
- return {
- ...item,
- flipped: true,
- unflippedFor,
- };
- });
- },
- [batchUpdateItems, currentUser.uid]
- );
- // Reveal for player only
- const toggleFlipSelf = React.useCallback(
- async (itemIds) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- const flippedSelfCount = items.filter(
- ({ unflippedFor }) =>
- Array.isArray(unflippedFor) && unflippedFor.includes(currentUser.uid)
- ).length;
- let flipSelf = true;
- if (flippedSelfCount > ids.length / 2) {
- flipSelf = false;
- }
- setFlipSelf(ids, { flipSelf });
- },
- [getItemListOrSelected, setFlipSelf, currentUser.uid]
- );
- const remove = React.useCallback(
- async (itemIds) => {
- const [ids] = await getItemListOrSelected(itemIds);
- removeItems(ids);
- },
- [getItemListOrSelected, removeItems]
- );
- const cloneItem = React.useCallback(
- async (itemIds) => {
- const [, items] = await getItemListOrSelected(itemIds);
- items.forEach((itemToClone) => {
- const newItem = JSON.parse(JSON.stringify(itemToClone));
- newItem.id = uid();
- delete newItem.move;
- pushItem(newItem, itemToClone.id);
- });
- },
- [getItemListOrSelected, pushItem]
- );
- const shuffleBag = React.useCallback(
- async (itemIds) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- batchUpdateItems(ids, (item) => {
- /* Prevent modification if item doesn't store other items */
- if (item.storedItems) {
- const elem = getItemElement(item.id);
- elem.firstChild.className = "hvr-wobble-horizontal";
- const shuffledItems = shuffleArray([...item.storedItems]);
- return {
- ...item,
- storedItems: shuffledItems,
- };
- } else {
- return item;
- }
- });
- //play audio only if at least a component has been shuffled
- let shuffled = false;
- items.forEach((item) => {
- if (item.storedItems) {
- shuffled = true;
- }
- });
- if (shuffled)
- playAudio(shuffleAudio, 0.5);
- },
- [getItemListOrSelected, batchUpdateItems]
- );
- const emptyBag = React.useCallback(
- async (itemIds) => {
- const [ids, items] = await getItemListOrSelected(itemIds);
- items.forEach((item) => {
- if (item.storedItems) {
- const elem = getItemElement(item.id);
- const extractedItems = item.storedItems.map((sItem) => {
- const extractedItem = {
- ...sItem,
- x: item.x + elem.clientWidth / 3.0,
- y: item.y + elem.clientHeight / 3.0,
- }
- return extractedItem;
- })
- pushItems(extractedItems);
- }
- });
- batchUpdateItems(ids, (item) => {
- /* Prevent modification if item doesn't store other items */
- if (item.storedItems) {
- return {
- ...item,
- storedItems: [],
- };
- } else {
- return item;
- }
- });
- },
- [getItemListOrSelected, batchUpdateItems, pushItems]
- );
- const actionMap = React.useMemo(() => {
- const actions = {
- flip: {
- action: () => toggleFlip,
- label: t("Reveal") + "/" + t("Hide"),
- shortcut: "f",
- icon: flipIcon,
- },
- reveal: {
- action: () => (itemIds) => setFlip(itemIds, { flip: false }),
- label: t("Reveal"),
- icon: flipIcon,
- },
- hide: {
- action: () => (itemIds) => setFlip(itemIds, { flip: true }),
- label: t("Hide"),
- icon: flipIcon,
- },
- flipSelf: {
- action: () => toggleFlipSelf,
- label: t("Reveal for me"),
- shortcut: "o",
- icon: seeIcon,
- },
- revealSelf: {
- action: () => (itemIds) => setFlipSelf(itemIds, { flipSelf: true }),
- label: t("Reveal for me"),
- icon: seeIcon,
- },
- hideSelf: {
- action: () => (itemIds) => setFlipSelf(itemIds, { flipSelf: false }),
- label: t("Hide for me"),
- icon: seeIcon,
- },
- tap: {
- action: () => toggleTap,
- label: t("Tap") + "/" + t("Untap"),
- shortcut: "t",
- icon: tapIcon,
- },
- stackToCenter: {
- action: () => stackToCenter,
- label: t("Stack To Center"),
- shortcut: "c",
- multiple: true,
- icon: stackToCenterIcon,
- },
- stack: {
- action: () => stackToTopLeft,
- label: t("Stack To Top Left"),
- shortcut: "p",
- multiple: true,
- icon: stackToTopLeftIcon,
- },
- alignAsLine: {
- action: () => alignAsLine,
- label: t("Align as line"),
- multiple: true,
- icon: alignAsLineIcon,
- },
- alignAsSquare: {
- action: () => alignAsSquare,
- label: t("Align as square"),
- multiple: true,
- icon: alignAsSquareIcon,
- },
- roll: {
- action: () => roll,
- label: t("Roll"),
- shortcut: "r",
- icon: rollIcon,
- },
- shuffle: {
- action: () => shuffleItems,
- label: t("Shuffle"),
- shortcut: "z",
- multiple: true,
- icon: shuffleIcon,
- },
- randomlyRotate: {
- action: ({ angle = 25, maxRotateCount = 0 } = {}) => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle,
- maxRotateCount,
- }),
- label: ({ angle = 25 } = {}) =>
- t("Rotate randomly {{angle}}°", { angle }),
- genericLabel: t("Rotate randomly"),
- multiple: false,
- icon: rotateIcon,
- form: RandomlyRotateActionForm,
- },
- randomlyRotate30: {
- action: () => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle: 30,
- maxRotateCount: 11,
- }),
- label: t("Rotate randomly 30"),
- multiple: false,
- icon: rotateIcon,
- },
- randomlyRotate45: {
- action: () => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle: 45,
- maxRotateCount: 7,
- }),
- label: t("Rotate randomly 45"),
- shortcut: "",
- multiple: false,
- icon: rotateIcon,
- },
- randomlyRotate60: {
- action: () => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle: 60,
- maxRotateCount: 5,
- }),
- label: t("Rotate randomly 60"),
- shortcut: "",
- multiple: false,
- icon: rotateIcon,
- },
- randomlyRotate90: {
- action: () => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle: 90,
- maxRotateCount: 3,
- }),
- label: t("Rotate randomly 90"),
- shortcut: "",
- multiple: false,
- icon: rotateIcon,
- },
- randomlyRotate180: {
- action: () => (itemIds) =>
- randomlyRotateSelectedItems(itemIds, {
- angle: 180,
- maxRotateCount: 1,
- }),
- label: t("Rotate randomly 180"),
- shortcut: "",
- multiple: false,
- icon: rotateIcon,
- },
- rotate: {
- action: ({ angle = 25 } = {}) => (itemIds) =>
- rotate(itemIds, { angle }),
- label: ({ angle = 25 } = {}) => t("Rotate {{angle}}°", { angle }),
- genericLabel: t("Rotate"),
- shortcut: "r",
- icon: rotateIcon,
- form: RotateActionForm,
- },
- rotate30: {
- action: () => (itemIds) => rotate(itemIds, { angle: 30 }),
- label: t("Rotate 30"),
- shortcut: "r",
- icon: rotateIcon,
- },
- rotate45: {
- action: () => (itemIds) => rotate(itemIds, { angle: 45 }),
- label: t("Rotate 45"),
- shortcut: "r",
- icon: rotateIcon,
- },
- rotate60: {
- action: () => (itemIds) => rotate(itemIds, { angle: 60 }),
- label: t("Rotate 60"),
- shortcut: "r",
- icon: rotateIcon,
- },
- rotate90: {
- action: () => (itemIds) => rotate(itemIds, { angle: 90 }),
- label: t("Rotate 90"),
- shortcut: "r",
- icon: rotateIcon,
- },
- rotate180: {
- action: () => (itemIds) => rotate(itemIds, { angle: 180 }),
- label: t("Rotate 180"),
- shortcut: "r",
- icon: rotateIcon,
- },
- clone: {
- action: () => cloneItem,
- label: t("Clone"),
- shortcut: "c",
- disableDblclick: true,
- icon: duplicateIcon,
- },
- lock: {
- action: () => toggleLock,
- label: t("Unlock") + "/" + t("Lock"),
- shortcut: "l",
- disableDblclick: true,
- edit: true,
- icon: lockIcon,
- },
- remove: {
- action: () => remove,
- label: t("Remove all"),
- shortcut: "Delete",
- edit: true,
- disableDblclick: true,
- icon: deleteIcon,
- },
- shuffleBag: {
- action: () => shuffleBag,
- label: t("Shuffle Bag"),
- shortcut: "b",
- disableDblclick: true,
- icon: shuffleBagIcon,
- },
- emptyBag: {
- action: () => emptyBag,
- label: t("Empty Bag"),
- shortcut: "x",
- disableDblclick: true,
- icon: emptyBagIcon,
- },
- };
- return Object.fromEntries(
- Object.entries(actions).map(([key, value]) => {
- const { label } = value;
- if (typeof label === "string") {
- value.label = () => label;
- }
- return [key, value];
- })
- );
- }, [
- alignAsLine,
- alignAsSquare,
- cloneItem,
- randomlyRotateSelectedItems,
- remove,
- roll,
- rotate,
- setFlip,
- setFlipSelf,
- shuffleItems,
- stackToCenter,
- stackToTopLeft,
- t,
- toggleFlip,
- toggleFlipSelf,
- toggleLock,
- toggleTap,
- shuffleBag,
- emptyBag,
- ]);
- return {
- randomlyRotate: randomlyRotateSelectedItems,
- remove,
- roll,
- rotate,
- stack: stackToTopLeft,
- setFlip,
- setFlipSelf,
- toggleFlip,
- toggleFlipSelf,
- toggleLock,
- toggleTap,
- snapToPoint,
- shuffle: shuffleItems,
- shuffleBag,
- emptyBag,
- actionMap,
- };
- };
- export default useGameItemActions;
|