Browse Source

Add dice with random images

Jeremie Pardou-Piquemal 2 years ago
parent
commit
11b7c4562e

+ 3 - 3
package-lock.json

@@ -9299,9 +9299,9 @@
       }
     },
     "react-sync-board": {
-      "version": "0.1.3",
-      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.1.3.tgz",
-      "integrity": "sha512-U1IrfgRL9fQ7zRKTw9JFpgqMYrmXxURV16h+llQxMgyO2mBMub3lSeJjdiKUX9Ft5FIrDNVY+49vxJuvklSyhw==",
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.1.4.tgz",
+      "integrity": "sha512-Ryg7M/0iuKGGyrPpMqjhXbcNw7SNH6OH3YLbZxQlu7chfKqxcSFsRkXDiEK+6/sBUf8Aia0yrc4urVfSiGLrGQ==",
       "requires": {
         "@emotion/react": "^11.4.0",
         "@emotion/styled": "^11.3.0",

+ 1 - 1
package.json

@@ -33,7 +33,7 @@
     "react-query": "^3.13.4",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
-    "react-sync-board": "^0.1.3",
+    "react-sync-board": "^0.1.4",
     "react-toastify": "^6.1.0",
     "react-useportal": "^1.0.14",
     "recoil": "^0.3.1",

File diff suppressed because it is too large
+ 60 - 0
public/game_assets/dice/five.svg


File diff suppressed because it is too large
+ 60 - 0
public/game_assets/dice/four.svg


File diff suppressed because it is too large
+ 18 - 0
public/game_assets/dice/one.svg


File diff suppressed because it is too large
+ 60 - 0
public/game_assets/dice/six.svg


File diff suppressed because it is too large
+ 60 - 0
public/game_assets/dice/three.svg


File diff suppressed because it is too large
+ 60 - 0
public/game_assets/dice/two.svg


+ 94 - 0
src/gameComponents/DiceImage.jsx

@@ -0,0 +1,94 @@
+import React, { memo } from "react";
+import styled from "styled-components";
+import { useItemInteraction } from "react-sync-board";
+import { media2Url } from "../mediaLibrary";
+
+const DicePane = styled.div`
+  line-height: 0;
+  img {
+    ${({ width }) => (width ? `width: ${width}px;` : "")}
+    ${({ height }) =>
+      height ? `height: ${height}px;` : ""}
+    pointer-events: none;
+  }
+`;
+
+const getRandomInt = (sides) => {
+  let min = 1;
+  let max = Math.ceil(sides);
+  return Math.floor(Math.random() * max) + min;
+};
+
+const defaultDiceImages = [
+  "/game_assets/dice/one.svg",
+  "/game_assets/dice/two.svg",
+  "/game_assets/dice/three.svg",
+  "/game_assets/dice/four.svg",
+  "/game_assets/dice/five.svg",
+  "/game_assets/dice/six.svg",
+];
+
+const Dice = ({
+  id,
+  value = 0,
+  side = 6,
+  images = defaultDiceImages,
+  width = 50,
+  height = 50,
+  rollOnDblClick = false,
+  setState,
+}) => {
+  const { register } = useItemInteraction("place");
+  const diceWrapper = React.useRef(null);
+
+  const roll = React.useCallback(() => {
+    diceWrapper.current.className = "hvr-wobble-horizontal";
+    const simulateRoll = (nextTimeout) => {
+      setState((prevState) => ({
+        ...prevState,
+        value: getRandomInt(side - 1),
+      }));
+      if (nextTimeout < 200) {
+        setTimeout(
+          () => simulateRoll(nextTimeout + getRandomInt(30)),
+          nextTimeout
+        );
+      }
+    };
+    simulateRoll(100);
+  }, [setState, side]);
+
+  const removeClass = (e) => {
+    e.target.className = "";
+  };
+
+  React.useEffect(() => {
+    const unregisterList = [];
+    if (!rollOnDblClick) {
+      const rollOnPlace = (itemIds) => {
+        if (itemIds.includes(id)) {
+          roll();
+        }
+      };
+
+      unregisterList.push(register(rollOnPlace));
+    }
+    return () => {
+      unregisterList.forEach((callback) => callback());
+    };
+  }, [roll, register, rollOnDblClick, id]);
+
+  return (
+    <div onAnimationEnd={removeClass} ref={diceWrapper}>
+      <DicePane
+        width={width}
+        height={height}
+        onDoubleClick={() => (rollOnDblClick ? roll() : null)}
+      >
+        {images[value] && <img src={media2Url(images[value])} />}
+      </DicePane>
+    </div>
+  );
+};
+
+export default memo(Dice);

+ 130 - 0
src/gameComponents/forms/DiceImageFormFields.jsx

@@ -0,0 +1,130 @@
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Field } from "react-final-form";
+
+import Label from "../../ui/formUtils/Label";
+import Hint from "../../ui/formUtils/Hint";
+import { ImageField } from "../../mediaLibrary";
+import { useItemActions } from "react-sync-board";
+import { nanoid } from "nanoid";
+
+const Form = ({ initialValues }) => {
+  const { t } = useTranslation();
+
+  const { batchUpdateItems } = useItemActions();
+
+  React.useEffect(() => {
+    if (!initialValues.images) {
+      // When selecting multiple images
+      return;
+    }
+    if (initialValues.images.length < initialValues.side) {
+      // Add emtpy element
+      batchUpdateItems([initialValues.id], (prevItem) => {
+        const newItem = { ...prevItem };
+        newItem.images = [...newItem.images];
+        newItem.images.push({ id: nanoid(), type: "empty" });
+        return newItem;
+      });
+    }
+    if (initialValues.images.length > initialValues.side) {
+      // remove element
+      batchUpdateItems([initialValues.id], (prevItem) => {
+        const newItem = { ...prevItem };
+        newItem.images = newItem.images.slice(0, newItem.images.length);
+        newItem.images.pop();
+        return newItem;
+      });
+    }
+  }, [
+    initialValues.side,
+    initialValues.images,
+    initialValues.id,
+    batchUpdateItems,
+  ]);
+
+  return (
+    <>
+      <Label>
+        {t("Label")}
+        <Field
+          name="label"
+          component="input"
+          initialValue={initialValues.label}
+        />
+      </Label>
+
+      <Label>
+        {t("Width")}
+        <Field
+          name="width"
+          component="input"
+          initialValue={initialValues.width}
+        >
+          {(props) => <input {...props.input} type="number" />}
+        </Field>
+      </Label>
+      <Label>
+        {t("Height")}
+        <Field
+          name="height"
+          component="input"
+          initialValue={initialValues.height}
+        >
+          {(props) => <input {...props.input} type="number" />}
+        </Field>
+      </Label>
+
+      <Label>
+        {t("Double click to roll")}
+        <Field
+          name="rollOnDblClick"
+          component="input"
+          type="checkbox"
+          initialValue={initialValues.rollOnDblClick}
+        />
+        <Hint>
+          {t("Check it to activate roll on double click instead of moving.")}
+        </Hint>
+      </Label>
+
+      <Label>
+        {t("Side count")}
+        <Field name="side" component="input" initialValue={initialValues.side}>
+          {(props) => {
+            const onChange = (newValue) => {
+              const parsed = parseInt(newValue.target.value, 10);
+              const nextValue = parsed > 1 ? parsed : 1;
+              props.input.onChange(nextValue);
+            };
+            return (
+              <input
+                value={props.input.value}
+                onChange={onChange}
+                type="number"
+              />
+            );
+          }}
+        </Field>
+      </Label>
+
+      {(initialValues.images || []).map(({ id }, index) => {
+        return (
+          <Label key={id}>
+            {t("Dice image {{index}}", { index: index + 1 })}
+            <Field
+              name={`images[${index}]`}
+              initialValue={initialValues.images[index]}
+            >
+              {({ input: { value, onChange } }) => {
+                return <ImageField value={value} onChange={onChange} />;
+              }}
+            </Field>
+          </Label>
+        );
+      })}
+    </>
+  );
+};
+
+export default Form;

+ 58 - 2
src/gameComponents/itemTemplates.js

@@ -9,6 +9,7 @@ import Token from "./Token";
 import Image from "./Image";
 import Counter from "./Counter";
 import Dice from "./Dice";
+import DiceImage from "./DiceImage";
 import Note from "./Note";
 import Zone from "./Zone";
 import Meeple from "./Meeple";
@@ -23,6 +24,7 @@ import RectFormFields from "./forms/RectFormFields";
 import CubeFormFields from "./forms/CubeFormFields";
 import RoundFormFields from "./forms/RoundFormFields";
 import DiceFormFields from "./forms/DiceFormFields";
+import DiceImageFormFields from "./forms/DiceImageFormFields";
 import NoteFormFields from "./forms/NoteFormFields";
 import ZoneFormFields from "./forms/ZoneFormFields";
 import TokenFormFields from "./forms/TokenFormFields";
@@ -32,6 +34,39 @@ import PawnFormFields from "./forms/PawnFormFields";
 import CheckerBoardFormFields from "./forms/CheckerBoardFormFields";
 import CylinderFormFields from "./forms/CylinderFormFields";
 
+const defaultDiceImages = () => [
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/one.svg",
+  },
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/two.svg",
+  },
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/three.svg",
+  },
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/four.svg",
+  },
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/five.svg",
+  },
+  {
+    id: nanoid(),
+    type: "external",
+    content: "/game_assets/dice/six.svg",
+  },
+];
+
 const itemTemplates = {
   rect: {
     component: Rect,
@@ -167,7 +202,9 @@ const itemTemplates = {
     availableActions: ["clone", "lock", "remove"],
     form: CheckerBoardFormFields,
     name: i18n.t("Checkerboard"),
-    template: { layer: -1 },
+    template: {
+      layer: -1,
+    },
   },
   image: {
     component: Image,
@@ -260,6 +297,23 @@ const itemTemplates = {
     name: i18n.t("Dice"),
     template: {},
   },
+  diceImage: {
+    component: DiceImage,
+    defaultActions: ["clone", "lock", "remove"],
+    availableActions: [
+      "clone",
+      "lock",
+      "remove",
+      "alignAsLine",
+      "alignAsSquare",
+    ],
+    form: DiceImageFormFields,
+    name: i18n.t("Image dice"),
+    template: () => ({
+      images: defaultDiceImages(),
+      side: 6,
+    }),
+  },
   note: {
     component: Note,
     defaultActions: ["shuffle", "clone", "lock", "remove"],
@@ -287,7 +341,9 @@ const itemTemplates = {
     ],
     form: ZoneFormFields,
     name: i18n.t("Zone"),
-    template: { layer: -1 },
+    template: {
+      layer: -1,
+    },
   },
 };
 

+ 5 - 1
src/i18n/en.json

@@ -224,5 +224,9 @@
   "{{min}} - {{max}} players": "{{min}} - {{max}} players",
   "{{min}}~{{max}} min": "{{min}}~{{max}} min",
   "~{{count}} min": "~{{count}} minute",
-  "~{{count}} min_plural": "{{count}} minutes"
+  "~{{count}} min_plural": "{{count}} minutes",
+  "Double click to roll": "Double click to roll",
+  "Check it to activate roll on double click instead of moving.": "check it to activate roll on double click instead of moving.",
+  "Dice image {{index}}": "Side #{{index}}",
+  "Image dice": "Image dice"
 }

+ 5 - 1
src/i18n/fr.json

@@ -224,5 +224,9 @@
   "{{min}} - {{max}} players": "{{min}} - {{max}} joueurs",
   "{{min}}~{{max}} min": "{{min}}~{{max}} minutes",
   "~{{count}} min": "~{{count}} minute",
-  "~{{count}} min_plural": "~{{count}} minutes"
+  "~{{count}} min_plural": "~{{count}} minutes",
+  "Double click to roll": "Double clic pour lancer",
+  "Check it to activate roll on double click instead of moving.": "Cocher pour activer le lancé au double clic à la place du déplacement.",
+  "Dice image {{index}}": "Face n° {{index}}",
+  "Image dice": "Dé avec image"
 }

+ 3 - 3
src/mediaLibrary/ImageField.jsx

@@ -44,15 +44,15 @@ const ImageField = ({ value, onChange }) => {
   }
 
   const handleInputChange = (e) => {
-    onChange({ type, content: e.target.value });
+    onChange({ ...value, type, content: e.target.value });
   };
 
   const handleTypeChange = (e) => {
-    onChange({ type: e.target.value, content: "" });
+    onChange({ ...value, type: e.target.value, content: "" });
   };
 
   const handleMediaSelect = (key) => {
-    onChange({ type: "local", content: key });
+    onChange({ ...value, type: "local", content: key });
   };
 
   const url = media2Url(value);

+ 11 - 3
src/mediaLibrary/ItemLibrary.jsx

@@ -67,7 +67,7 @@ const NewItem = memo(({ type, template, component: Component, name }) => {
 
   const addItem = React.useCallback(async () => {
     pushItem({
-      ...template,
+      ...(typeof template === "function" ? template() : template),
       id: nanoid(),
       type,
     });
@@ -76,7 +76,12 @@ const NewItem = memo(({ type, template, component: Component, name }) => {
   return (
     <StyledItem onClick={addItem}>
       <div>
-        <Component {...template} width={size} height={size} size={size} />
+        <Component
+          {...(typeof template === "function" ? template() : template)}
+          width={size}
+          height={size}
+          size={size}
+        />
         <span>{name}</span>
       </div>
     </StyledItem>
@@ -93,7 +98,10 @@ const SubItemList = ({ name, items }) => {
   const addItems = useRecoilCallback(
     async (itemsToAdd) => {
       pushItems(
-        itemsToAdd.map(({ template }) => ({ ...template, id: nanoid() }))
+        itemsToAdd.map(({ template }) => ({
+          ...(typeof template === "function" ? template() : template),
+          id: nanoid(),
+        }))
       );
     },
     [pushItems]

Some files were not shown because too many files changed in this diff