useGameItemActions.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  1. import React from "react";
  2. import { useTranslation } from "react-i18next";
  3. import { toast } from "react-toastify";
  4. import { useItemActions, useUsers, useSelectedItems } from "react-sync-board";
  5. import {
  6. shuffle as shuffleArray,
  7. randInt,
  8. uid,
  9. getItemElement,
  10. playAudio,
  11. } from "../utils";
  12. import RotateActionForm from "./forms/RotateActionForm";
  13. import RandomlyRotateActionForm from "./forms/RandomlyRotateActionForm";
  14. import deleteIcon from "../media/images/delete.svg";
  15. import stackToCenterIcon from "../media/images/stackToCenter.svg";
  16. import stackToTopLeftIcon from "../media/images/stackToTopLeft.svg";
  17. import alignAsLineIcon from "../media/images/alignAsLine.svg";
  18. import alignAsSquareIcon from "../media/images/alignAsSquare.svg";
  19. import duplicateIcon from "../media/images/duplicate.svg";
  20. import seeIcon from "../media/images/see.svg";
  21. import flipIcon from "../media/images/flip.svg";
  22. import lockIcon from "../media/images/lock.svg";
  23. import rotateIcon from "../media/images/rotate.svg";
  24. import shuffleIcon from "../media/images/shuffle.svg";
  25. import tapIcon from "../media/images/tap.svg";
  26. import rollIcon from "../media/images/rolling-dices.svg";
  27. import emptyBagIcon from "../media/images/emptybag.svg"
  28. import bagExtractIcon from "../media/images/bagextract.svg"
  29. import shuffleBagIcon from "../media/images/shufflebag.svg"
  30. import flipAudio from "../media/audio/flip.wav?url";
  31. import rollAudio from "../media/audio/roll.wav?url";
  32. import shuffleAudio from "../media/audio/shuffle.wav?url";
  33. import useLocalStorage from "../hooks/useLocalStorage";
  34. export const useGameItemActions = () => {
  35. const {
  36. batchUpdateItems,
  37. removeItems,
  38. pushItem,
  39. pushItems,
  40. reverseItemsOrder,
  41. swapItems,
  42. getItems,
  43. } = useItemActions();
  44. const { t } = useTranslation();
  45. const [isFirstLock, setIsFirstLock] = useLocalStorage("isFirstLock", true);
  46. const { currentUser } = useUsers();
  47. const selectedItems = useSelectedItems();
  48. const getItemListOrSelected = React.useCallback(
  49. async (itemIds) => {
  50. if (itemIds) {
  51. return [itemIds, await getItems(itemIds)];
  52. } else {
  53. return [selectedItems, await getItems(selectedItems)];
  54. }
  55. },
  56. [getItems, selectedItems]
  57. );
  58. const isMountedRef = React.useRef(false);
  59. React.useEffect(() => {
  60. // Mounted guard
  61. isMountedRef.current = true;
  62. return () => {
  63. isMountedRef.current = false;
  64. };
  65. }, []);
  66. // Stack selection to Center
  67. const stackToCenter = React.useCallback(
  68. async (
  69. itemIds,
  70. {
  71. stackThicknessMin = 0.5,
  72. stackThicknessMax = 1,
  73. limitCardsNumber = 32,
  74. } = {}
  75. ) => {
  76. const [ids, items] = await getItemListOrSelected(itemIds);
  77. // Rule to manage thickness of the stack.
  78. let stackThickness = stackThicknessMax;
  79. if (items.length >= limitCardsNumber) {
  80. stackThickness = stackThicknessMin;
  81. }
  82. // To avoid displacement effects.
  83. let isSameGap = true;
  84. for (let i = 1; i < items.length; i++) {
  85. if (Math.abs(items[i].x - items[i - 1].x) != stackThickness) {
  86. isSameGap = false;
  87. break;
  88. }
  89. if (Math.abs(items[i].y - items[i - 1].y) != stackThickness) {
  90. isSameGap = false;
  91. break;
  92. }
  93. }
  94. if (isSameGap == true) {
  95. return;
  96. }
  97. // Compute middle position
  98. const minMax = { min: {}, max: {} };
  99. minMax.min.x = Math.min(...items.map(({ x }) => x));
  100. minMax.min.y = Math.min(...items.map(({ y }) => y));
  101. minMax.max.x = Math.max(
  102. ...items.map(({ x, id }) => x + getItemElement(id).clientWidth)
  103. );
  104. minMax.max.y = Math.max(
  105. ...items.map(({ y, id }) => y + getItemElement(id).clientHeight)
  106. );
  107. const { clientWidth, clientHeight } = getItemElement(items[0].id);
  108. let newX =
  109. minMax.min.x + (minMax.max.x - minMax.min.x) / 2 - clientWidth / 2;
  110. let newY =
  111. minMax.min.y + (minMax.max.y - minMax.min.y) / 2 - clientHeight / 2;
  112. batchUpdateItems(ids, (item) => {
  113. const newItem = {
  114. ...item,
  115. x: newX,
  116. y: newY,
  117. };
  118. newX += stackThickness;
  119. newY -= stackThickness;
  120. return newItem;
  121. });
  122. },
  123. [batchUpdateItems, getItemListOrSelected]
  124. );
  125. // Stack selection to Top Left
  126. const stackToTopLeft = React.useCallback(
  127. async (
  128. itemIds,
  129. {
  130. stackThicknessMin = 0.5,
  131. stackThicknessMax = 1,
  132. limitCardsNumber = 32,
  133. } = {}
  134. ) => {
  135. const [ids, items] = await getItemListOrSelected(itemIds);
  136. let { x: newX, y: newY } = items[0];
  137. // Rule to manage thickness of the stack.
  138. let stackThickness = stackThicknessMax;
  139. if (items.length >= limitCardsNumber) {
  140. stackThickness = stackThicknessMin;
  141. }
  142. batchUpdateItems(ids, (item) => {
  143. const newItem = {
  144. ...item,
  145. x: newX,
  146. y: newY,
  147. };
  148. newX += stackThickness;
  149. newY -= stackThickness;
  150. return newItem;
  151. });
  152. },
  153. [batchUpdateItems, getItemListOrSelected]
  154. );
  155. // Align selection to a line
  156. const alignAsLine = React.useCallback(
  157. async (itemIds, { gapBetweenItems = 5 } = {}) => {
  158. // Negative value is possible for 'gapBetweenItems'.
  159. const [ids, items] = await getItemListOrSelected(itemIds);
  160. let { x: newX, y: newY } = items[0];
  161. batchUpdateItems(ids, (item) => {
  162. const { clientWidth } = getItemElement(item.id);
  163. const newItem = {
  164. ...item,
  165. x: newX,
  166. y: newY,
  167. };
  168. newX += clientWidth + gapBetweenItems;
  169. return newItem;
  170. });
  171. },
  172. [getItemListOrSelected, batchUpdateItems]
  173. );
  174. // Align selection to an array
  175. const alignAsSquare = React.useCallback(
  176. async (itemIds, { gapBetweenItems = 5 } = {}) => {
  177. // Negative value is possible for 'gapBetweenItems'.
  178. const [ids, items] = await getItemListOrSelected(itemIds);
  179. // Count number of elements
  180. const numberOfElements = items.length;
  181. const numberOfColumns = Math.ceil(Math.sqrt(numberOfElements));
  182. let { x: newX, y: newY } = items[0];
  183. let currentColumn = 1;
  184. batchUpdateItems(ids, (item) => {
  185. const { clientWidth, clientHeight } = getItemElement(item.id);
  186. const newItem = {
  187. ...item,
  188. x: newX,
  189. y: newY,
  190. };
  191. newX += clientWidth + gapBetweenItems;
  192. currentColumn += 1;
  193. if (currentColumn > numberOfColumns) {
  194. currentColumn = 1;
  195. newX = items[0].x;
  196. newY += clientHeight + gapBetweenItems;
  197. }
  198. return newItem;
  199. });
  200. },
  201. [getItemListOrSelected, batchUpdateItems]
  202. );
  203. const snapToPoint = React.useCallback(
  204. async (itemIds, { x, y } = {}) => {
  205. batchUpdateItems(itemIds, (item) => {
  206. const { clientWidth, clientHeight } = getItemElement(item.id);
  207. let newX = x - clientWidth / 2;
  208. let newY = y - clientHeight / 2;
  209. const newItem = {
  210. ...item,
  211. x: newX,
  212. y: newY,
  213. };
  214. return newItem;
  215. });
  216. },
  217. [batchUpdateItems]
  218. );
  219. const roll = React.useCallback(
  220. async (itemIds) => {
  221. const [ids] = await getItemListOrSelected(itemIds);
  222. ids.forEach((itemId) => {
  223. const elem = getItemElement(itemId);
  224. elem.firstChild.className = "hvr-wobble-horizontal";
  225. });
  226. const simulateRoll = (nextTimeout) => {
  227. batchUpdateItems(ids, (item) => {
  228. return {
  229. ...item,
  230. value: randInt(0, (item.side || 6) - 1),
  231. };
  232. });
  233. if (nextTimeout < 300) {
  234. setTimeout(
  235. () => simulateRoll(nextTimeout + randInt(10, 50)),
  236. nextTimeout
  237. );
  238. }
  239. };
  240. simulateRoll(100);
  241. playAudio(rollAudio, 0.4);
  242. },
  243. [batchUpdateItems, getItemListOrSelected]
  244. );
  245. const shuffleItems = React.useCallback(
  246. async (itemIds) => {
  247. const [ids] = await getItemListOrSelected(itemIds);
  248. ids.forEach((itemId) => {
  249. const elem = getItemElement(itemId);
  250. elem.firstChild.className = "hvr-wobble-horizontal";
  251. });
  252. const shuffledItems = shuffleArray([...ids]);
  253. swapItems(ids, shuffledItems);
  254. playAudio(shuffleAudio, 0.5);
  255. },
  256. [getItemListOrSelected, swapItems]
  257. );
  258. const randomlyRotateSelectedItems = React.useCallback(
  259. async (itemIds, { angle, maxRotateCount = 0 }) => {
  260. const [ids] = await getItemListOrSelected(itemIds);
  261. const maxRotate = maxRotateCount || Math.round(360 / angle);
  262. batchUpdateItems(ids, (item) => {
  263. const rotation =
  264. ((item.rotation || 0) + angle * randInt(0, maxRotate)) % 360;
  265. return { ...item, rotation };
  266. });
  267. },
  268. [getItemListOrSelected, batchUpdateItems]
  269. );
  270. // Tap/Untap elements
  271. const toggleTap = React.useCallback(
  272. async (itemIds) => {
  273. const [ids, items] = await getItemListOrSelected(itemIds);
  274. const tappedCount = items.filter(({ rotation }) => rotation === 90)
  275. .length;
  276. let untap = false;
  277. if (tappedCount > ids.length / 2) {
  278. untap = true;
  279. }
  280. batchUpdateItems(ids, (item) => ({
  281. ...item,
  282. rotation: untap ? 0 : 90,
  283. }));
  284. },
  285. [getItemListOrSelected, batchUpdateItems]
  286. );
  287. // Lock / unlock elements
  288. const toggleLock = React.useCallback(
  289. async (itemIds) => {
  290. const [ids] = await getItemListOrSelected(itemIds);
  291. batchUpdateItems(ids, (item) => ({
  292. ...item,
  293. locked: !item.locked,
  294. }));
  295. // Help user on first lock
  296. if (isFirstLock) {
  297. toast.info(
  298. t("You've locked your first element. Long click to select it again."),
  299. { autoClose: false }
  300. );
  301. setIsFirstLock(false);
  302. }
  303. },
  304. [getItemListOrSelected, batchUpdateItems, isFirstLock, t, setIsFirstLock]
  305. );
  306. // Flip or reveal items
  307. const setFlip = React.useCallback(
  308. async (itemIds, { flip = true, reverseOrder = true } = {}) => {
  309. batchUpdateItems(itemIds, (item) => ({
  310. ...item,
  311. flipped: flip,
  312. unflippedFor:
  313. !Array.isArray(item.unflippedFor) || item.unflippedFor.length > 0
  314. ? null
  315. : item.unflippedFor,
  316. }));
  317. if (reverseOrder) {
  318. reverseItemsOrder(itemIds);
  319. }
  320. playAudio(flipAudio, 0.2);
  321. },
  322. [batchUpdateItems, reverseItemsOrder]
  323. );
  324. // Toggle flip state
  325. const toggleFlip = React.useCallback(
  326. async (itemIds, { reverseOrder = true } = {}) => {
  327. const [ids, items] = await getItemListOrSelected(itemIds);
  328. const flippedCount = items.filter(({ flipped }) => flipped).length;
  329. setFlip(ids, {
  330. flip: flippedCount < ids.length / 2,
  331. reverseOrder,
  332. });
  333. },
  334. [getItemListOrSelected, setFlip]
  335. );
  336. // Rotate element
  337. const rotate = React.useCallback(
  338. async (itemIds, { angle }) => {
  339. const [ids] = await getItemListOrSelected(itemIds);
  340. batchUpdateItems(ids, (item) => ({
  341. ...item,
  342. rotation: (item.rotation || 0) + angle,
  343. }));
  344. },
  345. [getItemListOrSelected, batchUpdateItems]
  346. );
  347. // Reveal for player only
  348. const setFlipSelf = React.useCallback(
  349. async (itemIds, { flipSelf = true } = {}) => {
  350. batchUpdateItems(itemIds, (item) => {
  351. let { unflippedFor = [] } = item;
  352. if (!Array.isArray(item.unflippedFor)) {
  353. unflippedFor = [];
  354. }
  355. if (flipSelf && !unflippedFor.includes(currentUser.uid)) {
  356. unflippedFor = [...unflippedFor, currentUser.uid];
  357. }
  358. if (!flipSelf && unflippedFor.includes(currentUser.uid)) {
  359. unflippedFor = unflippedFor.filter((id) => id !== currentUser.uid);
  360. }
  361. return {
  362. ...item,
  363. flipped: true,
  364. unflippedFor,
  365. };
  366. });
  367. },
  368. [batchUpdateItems, currentUser.uid]
  369. );
  370. // Reveal for player only
  371. const toggleFlipSelf = React.useCallback(
  372. async (itemIds) => {
  373. const [ids, items] = await getItemListOrSelected(itemIds);
  374. const flippedSelfCount = items.filter(
  375. ({ unflippedFor }) =>
  376. Array.isArray(unflippedFor) && unflippedFor.includes(currentUser.uid)
  377. ).length;
  378. let flipSelf = true;
  379. if (flippedSelfCount > ids.length / 2) {
  380. flipSelf = false;
  381. }
  382. setFlipSelf(ids, { flipSelf });
  383. },
  384. [getItemListOrSelected, setFlipSelf, currentUser.uid]
  385. );
  386. const remove = React.useCallback(
  387. async (itemIds) => {
  388. const [ids] = await getItemListOrSelected(itemIds);
  389. removeItems(ids);
  390. },
  391. [getItemListOrSelected, removeItems]
  392. );
  393. const cloneItem = React.useCallback(
  394. async (itemIds) => {
  395. const [, items] = await getItemListOrSelected(itemIds);
  396. items.forEach((itemToClone) => {
  397. const newItem = JSON.parse(JSON.stringify(itemToClone));
  398. newItem.id = uid();
  399. delete newItem.move;
  400. pushItem(newItem, itemToClone.id);
  401. });
  402. },
  403. [getItemListOrSelected, pushItem]
  404. );
  405. const shuffleBag = React.useCallback(
  406. async (itemIds) => {
  407. const [ids, items] = await getItemListOrSelected(itemIds);
  408. batchUpdateItems(ids, (item) => {
  409. /* Prevent modification if item doesn't store other items */
  410. if (item.storedItems) {
  411. const elem = getItemElement(item.id);
  412. elem.firstChild.className = "hvr-wobble-horizontal";
  413. const shuffledItems = shuffleArray([...item.storedItems]);
  414. return {
  415. ...item,
  416. storedItems: shuffledItems,
  417. };
  418. } else {
  419. return item;
  420. }
  421. });
  422. //play audio only if at least a component has been shuffled
  423. let shuffled = false;
  424. items.forEach((item) => {
  425. if (item.storedItems) {
  426. shuffled = true;
  427. }
  428. });
  429. if (shuffled)
  430. playAudio(shuffleAudio, 0.5);
  431. },
  432. [getItemListOrSelected, batchUpdateItems]
  433. );
  434. const emptyBag = React.useCallback(
  435. async (itemIds) => {
  436. const [ids, items] = await getItemListOrSelected(itemIds);
  437. items.forEach((item) => {
  438. if (item.storedItems) {
  439. const elem = getItemElement(item.id);
  440. const extractedItems = item.storedItems.map((sItem) => {
  441. const extractedItem = {
  442. ...sItem,
  443. x: item.x + elem.clientWidth / 3.0,
  444. y: item.y + elem.clientHeight / 3.0,
  445. }
  446. return extractedItem;
  447. })
  448. pushItems(extractedItems);
  449. }
  450. });
  451. batchUpdateItems(ids, (item) => {
  452. /* Prevent modification if item doesn't store other items */
  453. if (item.storedItems) {
  454. return {
  455. ...item,
  456. storedItems: [],
  457. };
  458. } else {
  459. return item;
  460. }
  461. });
  462. },
  463. [getItemListOrSelected, batchUpdateItems, pushItems]
  464. );
  465. const actionMap = React.useMemo(() => {
  466. const actions = {
  467. flip: {
  468. action: () => toggleFlip,
  469. label: t("Reveal") + "/" + t("Hide"),
  470. shortcut: "f",
  471. icon: flipIcon,
  472. },
  473. reveal: {
  474. action: () => (itemIds) => setFlip(itemIds, { flip: false }),
  475. label: t("Reveal"),
  476. icon: flipIcon,
  477. },
  478. hide: {
  479. action: () => (itemIds) => setFlip(itemIds, { flip: true }),
  480. label: t("Hide"),
  481. icon: flipIcon,
  482. },
  483. flipSelf: {
  484. action: () => toggleFlipSelf,
  485. label: t("Reveal for me"),
  486. shortcut: "o",
  487. icon: seeIcon,
  488. },
  489. revealSelf: {
  490. action: () => (itemIds) => setFlipSelf(itemIds, { flipSelf: true }),
  491. label: t("Reveal for me"),
  492. icon: seeIcon,
  493. },
  494. hideSelf: {
  495. action: () => (itemIds) => setFlipSelf(itemIds, { flipSelf: false }),
  496. label: t("Hide for me"),
  497. icon: seeIcon,
  498. },
  499. tap: {
  500. action: () => toggleTap,
  501. label: t("Tap") + "/" + t("Untap"),
  502. shortcut: "t",
  503. icon: tapIcon,
  504. },
  505. stackToCenter: {
  506. action: () => stackToCenter,
  507. label: t("Stack To Center"),
  508. shortcut: "c",
  509. multiple: true,
  510. icon: stackToCenterIcon,
  511. },
  512. stack: {
  513. action: () => stackToTopLeft,
  514. label: t("Stack To Top Left"),
  515. shortcut: "p",
  516. multiple: true,
  517. icon: stackToTopLeftIcon,
  518. },
  519. alignAsLine: {
  520. action: () => alignAsLine,
  521. label: t("Align as line"),
  522. multiple: true,
  523. icon: alignAsLineIcon,
  524. },
  525. alignAsSquare: {
  526. action: () => alignAsSquare,
  527. label: t("Align as square"),
  528. multiple: true,
  529. icon: alignAsSquareIcon,
  530. },
  531. roll: {
  532. action: () => roll,
  533. label: t("Roll"),
  534. shortcut: "r",
  535. icon: rollIcon,
  536. },
  537. shuffle: {
  538. action: () => shuffleItems,
  539. label: t("Shuffle"),
  540. shortcut: "z",
  541. multiple: true,
  542. icon: shuffleIcon,
  543. },
  544. randomlyRotate: {
  545. action: ({ angle = 25, maxRotateCount = 0 } = {}) => (itemIds) =>
  546. randomlyRotateSelectedItems(itemIds, {
  547. angle,
  548. maxRotateCount,
  549. }),
  550. label: ({ angle = 25 } = {}) =>
  551. t("Rotate randomly {{angle}}°", { angle }),
  552. genericLabel: t("Rotate randomly"),
  553. multiple: false,
  554. icon: rotateIcon,
  555. form: RandomlyRotateActionForm,
  556. },
  557. randomlyRotate30: {
  558. action: () => (itemIds) =>
  559. randomlyRotateSelectedItems(itemIds, {
  560. angle: 30,
  561. maxRotateCount: 11,
  562. }),
  563. label: t("Rotate randomly 30"),
  564. multiple: false,
  565. icon: rotateIcon,
  566. },
  567. randomlyRotate45: {
  568. action: () => (itemIds) =>
  569. randomlyRotateSelectedItems(itemIds, {
  570. angle: 45,
  571. maxRotateCount: 7,
  572. }),
  573. label: t("Rotate randomly 45"),
  574. shortcut: "",
  575. multiple: false,
  576. icon: rotateIcon,
  577. },
  578. randomlyRotate60: {
  579. action: () => (itemIds) =>
  580. randomlyRotateSelectedItems(itemIds, {
  581. angle: 60,
  582. maxRotateCount: 5,
  583. }),
  584. label: t("Rotate randomly 60"),
  585. shortcut: "",
  586. multiple: false,
  587. icon: rotateIcon,
  588. },
  589. randomlyRotate90: {
  590. action: () => (itemIds) =>
  591. randomlyRotateSelectedItems(itemIds, {
  592. angle: 90,
  593. maxRotateCount: 3,
  594. }),
  595. label: t("Rotate randomly 90"),
  596. shortcut: "",
  597. multiple: false,
  598. icon: rotateIcon,
  599. },
  600. randomlyRotate180: {
  601. action: () => (itemIds) =>
  602. randomlyRotateSelectedItems(itemIds, {
  603. angle: 180,
  604. maxRotateCount: 1,
  605. }),
  606. label: t("Rotate randomly 180"),
  607. shortcut: "",
  608. multiple: false,
  609. icon: rotateIcon,
  610. },
  611. rotate: {
  612. action: ({ angle = 25 } = {}) => (itemIds) =>
  613. rotate(itemIds, { angle }),
  614. label: ({ angle = 25 } = {}) => t("Rotate {{angle}}°", { angle }),
  615. genericLabel: t("Rotate"),
  616. shortcut: "r",
  617. icon: rotateIcon,
  618. form: RotateActionForm,
  619. },
  620. rotate30: {
  621. action: () => (itemIds) => rotate(itemIds, { angle: 30 }),
  622. label: t("Rotate 30"),
  623. shortcut: "r",
  624. icon: rotateIcon,
  625. },
  626. rotate45: {
  627. action: () => (itemIds) => rotate(itemIds, { angle: 45 }),
  628. label: t("Rotate 45"),
  629. shortcut: "r",
  630. icon: rotateIcon,
  631. },
  632. rotate60: {
  633. action: () => (itemIds) => rotate(itemIds, { angle: 60 }),
  634. label: t("Rotate 60"),
  635. shortcut: "r",
  636. icon: rotateIcon,
  637. },
  638. rotate90: {
  639. action: () => (itemIds) => rotate(itemIds, { angle: 90 }),
  640. label: t("Rotate 90"),
  641. shortcut: "r",
  642. icon: rotateIcon,
  643. },
  644. rotate180: {
  645. action: () => (itemIds) => rotate(itemIds, { angle: 180 }),
  646. label: t("Rotate 180"),
  647. shortcut: "r",
  648. icon: rotateIcon,
  649. },
  650. clone: {
  651. action: () => cloneItem,
  652. label: t("Clone"),
  653. shortcut: "c",
  654. disableDblclick: true,
  655. icon: duplicateIcon,
  656. },
  657. lock: {
  658. action: () => toggleLock,
  659. label: t("Unlock") + "/" + t("Lock"),
  660. shortcut: "l",
  661. disableDblclick: true,
  662. edit: true,
  663. icon: lockIcon,
  664. },
  665. remove: {
  666. action: () => remove,
  667. label: t("Remove all"),
  668. shortcut: "Delete",
  669. edit: true,
  670. disableDblclick: true,
  671. icon: deleteIcon,
  672. },
  673. shuffleBag: {
  674. action: () => shuffleBag,
  675. label: t("Shuffle Bag"),
  676. shortcut: "b",
  677. disableDblclick: true,
  678. icon: shuffleBagIcon,
  679. },
  680. emptyBag: {
  681. action: () => emptyBag,
  682. label: t("Empty Bag"),
  683. shortcut: "x",
  684. disableDblclick: true,
  685. icon: emptyBagIcon,
  686. },
  687. };
  688. return Object.fromEntries(
  689. Object.entries(actions).map(([key, value]) => {
  690. const { label } = value;
  691. if (typeof label === "string") {
  692. value.label = () => label;
  693. }
  694. return [key, value];
  695. })
  696. );
  697. }, [
  698. alignAsLine,
  699. alignAsSquare,
  700. cloneItem,
  701. randomlyRotateSelectedItems,
  702. remove,
  703. roll,
  704. rotate,
  705. setFlip,
  706. setFlipSelf,
  707. shuffleItems,
  708. stackToCenter,
  709. stackToTopLeft,
  710. t,
  711. toggleFlip,
  712. toggleFlipSelf,
  713. toggleLock,
  714. toggleTap,
  715. shuffleBag,
  716. emptyBag,
  717. ]);
  718. return {
  719. randomlyRotate: randomlyRotateSelectedItems,
  720. remove,
  721. roll,
  722. rotate,
  723. stack: stackToTopLeft,
  724. setFlip,
  725. setFlipSelf,
  726. toggleFlip,
  727. toggleFlipSelf,
  728. toggleLock,
  729. toggleTap,
  730. snapToPoint,
  731. shuffle: shuffleItems,
  732. shuffleBag,
  733. emptyBag,
  734. actionMap,
  735. };
  736. };
  737. export default useGameItemActions;