Browse Source

Add demo games for learning purposes

Jeremie Pardou-Piquemal 1 year ago
parent
commit
b1eafc78f3

+ 7 - 7
package-lock.json

@@ -39,7 +39,7 @@
         "react-query": "^3.13.4",
         "react-router": "^5.2.0",
         "react-router-dom": "^5.2.0",
-        "react-sync-board": "^0.6.2",
+        "react-sync-board": "^0.7.0",
         "react-toastify": "^6.1.0",
         "react-useportal": "^1.0.14",
         "recoil": "^0.7.0",
@@ -8878,9 +8878,9 @@
       "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
     },
     "node_modules/react-sync-board": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.6.2.tgz",
-      "integrity": "sha512-Yf1m2xD4Ih7ysfBqRi9kg/K0OzFN1LomZBue/FnMB2sl07xmJestjhLBqHco6bdpj4debdU7KiImKfq7A4WLeg==",
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.7.0.tgz",
+      "integrity": "sha512-blsmSFLKPP9OJW5UMBiBGcN6dSgHT+RTCxfcXQYOUCPh4qil7/RypdBJxwPvv0RCoQDogiJdPMi9lULJ3tD01w==",
       "dependencies": {
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",
@@ -17258,9 +17258,9 @@
       }
     },
     "react-sync-board": {
-      "version": "0.6.2",
-      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.6.2.tgz",
-      "integrity": "sha512-Yf1m2xD4Ih7ysfBqRi9kg/K0OzFN1LomZBue/FnMB2sl07xmJestjhLBqHco6bdpj4debdU7KiImKfq7A4WLeg==",
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/react-sync-board/-/react-sync-board-0.7.0.tgz",
+      "integrity": "sha512-blsmSFLKPP9OJW5UMBiBGcN6dSgHT+RTCxfcXQYOUCPh4qil7/RypdBJxwPvv0RCoQDogiJdPMi9lULJ3tD01w==",
       "requires": {
         "@emotion/react": "^11.9.0",
         "@emotion/styled": "^11.8.1",

+ 1 - 1
package.json

@@ -34,7 +34,7 @@
     "react-query": "^3.13.4",
     "react-router": "^5.2.0",
     "react-router-dom": "^5.2.0",
-    "react-sync-board": "^0.6.2",
+    "react-sync-board": "^0.7.0",
     "react-toastify": "^6.1.0",
     "react-useportal": "^1.0.14",
     "recoil": "^0.7.0",

BIN
public/game_assets/default.png


BIN
public/game_assets/testgame.png


+ 16 - 6
src/gameComponents/Zone.jsx

@@ -21,20 +21,29 @@ const ZoneWrapper = styled.div`
     background-color: ${backgroundColor};
     border-radius: 5px;
     position: relative;
-    & > div {
+    & .zone__label {
       font-size: 1.5em;
-      letter-spacing: -3px;
       user-select: none;
       background-color: ${opacify(borderColor, 1)};
       position: absolute;
+      border-radius: 0.5em;
+      color: var(--color-darkGrey);
+    }
+
+    & .zone__label.left {
       padding: 1em 0em;
       top: 1em;
       left: -1em;
-      border-radius: 0.5em;
-      color: var(--color-darkGrey);
+      letter-spacing: -3px;
       writing-mode: vertical-rl;
       text-orientation: upright;
     }
+
+    & .zone__label.top {
+      padding: 0em 1em;
+      top: -1em;
+      left: 1em;
+    }
   `}
 `;
 
@@ -46,6 +55,7 @@ const Zone = ({
   borderColor,
   borderStyle,
   backgroundColor,
+  labelPosition = "left",
 }) => {
   const { register } = useItemInteraction("place");
   const zoneRef = React.useRef(null);
@@ -56,6 +66,7 @@ const Zone = ({
       const insideItems = itemIds.filter((itemId) =>
         isItemInsideElement(getItemElement(itemId), zoneRef.current)
       );
+
       if (!insideItems.length) return;
 
       const onItemActions = onItem.map((action) => {
@@ -64,7 +75,6 @@ const Zone = ({
         }
         return action;
       });
-
       onItemActions.forEach(({ name, args }) => {
         switch (name) {
           case "reveal":
@@ -107,7 +117,7 @@ const Zone = ({
       borderColor={borderColor}
       backgroundColor={backgroundColor}
     >
-      <div>{label}</div>
+      <div className={`zone__label ${labelPosition}`}>{label}</div>
     </ZoneWrapper>
   );
 };

+ 13 - 0
src/gameComponents/forms/ZoneFormFields.jsx

@@ -100,6 +100,19 @@ const Form = ({ initialValues }) => {
         </Field>
       </Label>
 
+      <Label>
+        {t("Label position")}
+        <Field
+          name="labelPosition"
+          component="select"
+          initialValue={initialValues.labelPosition || "left"}
+          style={{ width: "10em" }}
+        >
+          <option value="left">{t("Left")}</option>
+          <option value="top">{t("Top")}</option>
+        </Field>
+      </Label>
+
       <h3>{t("Interactions")}</h3>
       <Hint>{t("Interaction help")}</Hint>
       <Label>

+ 255 - 0
src/games/demo_en.json

@@ -0,0 +1,255 @@
+{
+  "items": [
+    {
+      "id": "taBETVuRcF",
+      "type": "note",
+      "x": 124,
+      "y": -259,
+      "value": "- Two fingers to move the board\n- Pinch to zoom",
+      "label": "With a touchpad"
+    },
+    {
+      "id": "g5fAVbjPH4",
+      "layer": -1,
+      "type": "zone",
+      "x": -267.5,
+      "y": -288.5,
+      "label": "Navigation",
+      "width": "1070.8",
+      "height": "328.8"
+    },
+    {
+      "id": "x3wQ2rR5vS",
+      "type": "note",
+      "x": -190,
+      "y": -259,
+      "value": "- Middle or right click on the board to move the board\n- Wheel to zoom",
+      "label": "With a mouse"
+    },
+    {
+      "id": "NrcuGDrmW8",
+      "type": "note",
+      "x": -190,
+      "y": 852.5,
+      "value": "- When an item is selected, click on the action icon inside the action menu just above the item\n- You can double click on an item to trigger his main action\n- You also can use the keyboard shortcuts shown when you hover an action in the menu",
+      "label": "A single item",
+      "width": "300.0",
+      "height": "295.1",
+      "color": "#ffc",
+      "fontSize": 20,
+      "fontFamily": "Roboto",
+      "textColor": "#000",
+      "actions": [
+        { "name": "shuffle" },
+        { "name": "clone" },
+        { "name": "lock" },
+        { "name": "remove" }
+      ]
+    },
+    {
+      "id": "rdmKGYgYqX",
+      "type": "note",
+      "x": -189.5,
+      "y": 1474.5,
+      "value": "- Click to select a card just above\n- Double click to flip it\n- Try another action of the menu\n- Try to do action with keyboard shortcuts\n- Select all cards and try the new actions\n\n",
+      "label": "Let's try it",
+      "width": "331.0",
+      "height": "255.2",
+      "color": "rgba(184, 233, 134, 1)"
+    },
+    {
+      "id": "Nv8WebDYgD",
+      "type": "image",
+      "x": 197,
+      "y": 1645.5,
+      "content": { "type": "external", "content": "/game_assets/BH.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null
+    },
+    {
+      "id": "nU3xCrYnPr",
+      "type": "note",
+      "x": 439,
+      "y": -259,
+      "value": "- Two fingers to move the board\n- Pinch to zoom",
+      "label": "With a toushscreen"
+    },
+    {
+      "id": "nsYMqz673q",
+      "type": "note",
+      "x": -189.5,
+      "y": 1234.5,
+      "value": "- The last icon is not an action but toggle the edit panel\n- You can edit every aspect of this item this way\n- You shouldn't need it while playing but can be useful sometimes",
+      "label": "Item edition",
+      "width": "611.3",
+      "height": "103.2"
+    },
+    {
+      "id": "GzxEj35Jy6",
+      "layer": -1,
+      "type": "zone",
+      "x": -261,
+      "y": 813,
+      "label": "Item interaction",
+      "width": "730.4",
+      "height": "1037.8"
+    },
+    {
+      "id": "rHA3e6TbM2",
+      "layer": -1,
+      "type": "zone",
+      "x": -259,
+      "y": 123,
+      "label": "Item manipulation",
+      "width": "726.1",
+      "height": "653.6"
+    },
+    {
+      "id": "5KujHvza7f",
+      "type": "note",
+      "x": 135,
+      "y": 152,
+      "value": "- You can start to select multiple items by clicking outside of any item and moving the mouse.\n- Then you can move all items",
+      "label": "Multiple items"
+    },
+    {
+      "id": "WE8k4Tv7BS",
+      "type": "note",
+      "x": -199,
+      "y": 151.5,
+      "value": "- You can drag and drop item by clicking on any unlocked item.\n- You can select one item by clicking on it\n- you can clear the selection by clicking outside an item",
+      "label": "A single item",
+      "width": "300.0",
+      "height": "201.3"
+    },
+    {
+      "id": "6nMUFs4FZK",
+      "type": "note",
+      "x": -198,
+      "y": 451.5,
+      "value": "- Try to move the meeples on the right\n- Try to select them all\n- Try to move them all at the same time",
+      "label": "Let's try it",
+      "width": "299.7",
+      "height": "202.5",
+      "color": "rgba(184, 233, 134, 1)"
+    },
+    {
+      "id": "qdyg4sgfvn",
+      "type": "meeple",
+      "name": "Meeple rouge",
+      "color": "#D82735",
+      "x": 268,
+      "y": 488,
+      "actions": []
+    },
+    {
+      "id": "cFdSexMN79",
+      "type": "meeple",
+      "name": "Meeple orange",
+      "color": "#FF7435",
+      "x": 269,
+      "y": 558,
+      "actions": []
+    },
+    {
+      "id": "yGuZG5hGgE",
+      "type": "meeple",
+      "name": "Meeple jaune",
+      "color": "#F9DF00",
+      "x": 269,
+      "y": 633,
+      "actions": []
+    },
+    {
+      "id": "XSkQfZdat2",
+      "type": "note",
+      "x": 126.5,
+      "y": 851,
+      "value": "- Use the action menu for the selected items, only actions available for all items are displayed\n- You may have noticed that some actions are only visible when you've selected multiple items like, the shuffle action\n- You can also use keyboard shortcuts here\n",
+      "label": "Multiple items",
+      "width": "296.7",
+      "height": "300.1"
+    },
+    {
+      "id": "N3xNz4ksyV",
+      "type": "image",
+      "x": 260,
+      "y": 1480.5,
+      "content": { "type": "external", "content": "/game_assets/AS.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": [],
+      "rotation": 0
+    },
+    {
+      "id": "aPhbdhXuFt",
+      "type": "image",
+      "x": 324,
+      "y": 1645.5,
+      "content": { "type": "external", "content": "/game_assets/JC.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null,
+      "rotation": 0
+    }
+  ],
+  "board": {
+    "size": 2000,
+    "scale": 1,
+    "defaultName": "How to play?",
+    "bgType": "default",
+    "playerCount": [1, 9],
+    "duration": [],
+    "gridSize": 1,
+    "imageUrl": "/game_assets/default.png",
+    "keepTitle": true,
+    "initialBoardPosition": {
+      "top": 24700,
+      "left": 24750,
+      "width": 1000,
+      "height": 300
+    },
+    "defaultLanguage": "en",
+    "materialLanguage": "Multi-lang",
+    "defaultBaseline": "Learn how to play with Airboardgame",
+    "defaultDescription": "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    "translations": [
+      {
+        "language": "fr",
+        "name": "Comment jouer ?",
+        "baseline": "Apprenez à jouer avec Airboardgame",
+        "description": "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles."
+      }
+    ],
+    "published": true
+  },
+  "availableItems": []
+}

+ 276 - 0
src/games/demo_fr.json

@@ -0,0 +1,276 @@
+{
+  "items": [
+    {
+      "id": "g5fAVbjPH4",
+      "layer": -1,
+      "type": "zone",
+      "x": -267.5,
+      "y": -290.5,
+      "label": "Navigation",
+      "width": "1070.8",
+      "height": "328.8",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "taBETVuRcF",
+      "type": "note",
+      "x": 124,
+      "y": -256,
+      "value": "- Utilisez deux doigts pour déplacer le plateau\n- Pincez pour zoomer",
+      "label": "Avec un pavé tactile",
+      "locked": true
+    },
+    {
+      "id": "x3wQ2rR5vS",
+      "type": "note",
+      "x": -190,
+      "y": -256,
+      "value": "- Cliquez avec le bouton du milieu ou de gauche pour déplacer le plateau\n- La molette permet de zoomer",
+      "label": "Avec une souris",
+      "locked": true
+    },
+    {
+      "id": "nU3xCrYnPr",
+      "type": "note",
+      "x": 439,
+      "y": -256,
+      "value": "- Utilisez deux doigts pour déplacer le plateau\n- Pincez pour zoomer",
+      "label": "Avec un écran tactile",
+      "locked": true
+    },
+    {
+      "id": "qdyg4sgfvn",
+      "type": "meeple",
+      "name": "Meeple rouge",
+      "color": "#D82735",
+      "x": 258,
+      "y": 478,
+      "actions": []
+    },
+    {
+      "id": "cFdSexMN79",
+      "type": "meeple",
+      "name": "Meeple orange",
+      "color": "#FF7435",
+      "x": 259,
+      "y": 548,
+      "actions": []
+    },
+    {
+      "id": "yGuZG5hGgE",
+      "type": "meeple",
+      "name": "Meeple jaune",
+      "color": "#F9DF00",
+      "x": 259,
+      "y": 623,
+      "actions": []
+    },
+    {
+      "id": "XSkQfZdat2",
+      "type": "note",
+      "x": 123.5,
+      "y": 862,
+      "value": "- Utilisez le menu d'action pour tous les éléments sélectionnés. Seules les actions disponibles sur tous les éléments sélectionnés sont affichées\n- Certaines actions sont visibles uniquement quand vous sélectionnez plusieurs éléments comme l'action « mélanger »\n- Vous pouvez également utiliser les raccourcis clavier\n",
+      "label": "Plusieurs éléments",
+      "width": "296.7",
+      "height": "348.3",
+      "locked": true
+    },
+    {
+      "id": "NrcuGDrmW8",
+      "type": "note",
+      "x": -190,
+      "y": 864.5,
+      "value": "- Quand un élément est sélectionné, cliquez sur le menu d'action juste au dessus de l'élément\n- Vous pouvez double-cliquez pour effectuer son action principale\n- Vous pouvez également utiliser les raccourcis clavier qui s'affichent quand vous survolez une action dans le menu",
+      "label": "Un seul élément",
+      "width": "300.0",
+      "height": "347.2",
+      "color": "#ffc",
+      "fontSize": 20,
+      "fontFamily": "Roboto",
+      "textColor": "#000",
+      "actions": [
+        { "name": "shuffle" },
+        { "name": "clone" },
+        { "name": "lock" },
+        { "name": "remove" }
+      ],
+      "locked": true
+    },
+    {
+      "id": "rHA3e6TbM2",
+      "layer": -1,
+      "type": "zone",
+      "x": -262,
+      "y": 98,
+      "label": "Déplacement des éléments",
+      "width": "726.1",
+      "height": "653.6",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "WE8k4Tv7BS",
+      "type": "note",
+      "x": -206,
+      "y": 146.5,
+      "value": "- Vous pouvez déplacer un élément en cliquant dessus et en maintenant le bouton.\n- Vous pouvez sélectionner un unique élément en cliquant dessus\n- Vous pouvez tout désélectionner en cliquant en dehors d'un élément",
+      "label": "Un seul élément",
+      "width": "300.0",
+      "height": "224.6",
+      "locked": true
+    },
+    {
+      "id": "6nMUFs4FZK",
+      "type": "note",
+      "x": -204,
+      "y": 452.5,
+      "value": "- Essayez de déplacer un Meeple sur la droite\n- Essayez de les sélectionner tous\n- Essayez de les déplacer tous en même temp",
+      "label": "Testez-le",
+      "width": "299.7",
+      "height": "202.5",
+      "color": "rgba(184, 233, 134, 1)",
+      "locked": true
+    },
+    {
+      "id": "5KujHvza7f",
+      "type": "note",
+      "x": 125,
+      "y": 142,
+      "value": "- Pour sélectionner plusieurs élément cliquez en dehors d'un élément et tout en maintenant le bouton déplacez la souris. Relachez quand vous êtes satisfait\n- Vous pouvez alors déplacer tous les élements en même temps",
+      "label": "Plusieurs éléments",
+      "width": "300.0",
+      "height": "224.0",
+      "locked": true
+    },
+    {
+      "id": "Nv8WebDYgD",
+      "type": "image",
+      "x": 194,
+      "y": 1710.5,
+      "content": { "type": "external", "content": "/game_assets/BH.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null
+    },
+    {
+      "id": "N3xNz4ksyV",
+      "type": "image",
+      "x": 257,
+      "y": 1545.5,
+      "content": { "type": "external", "content": "/game_assets/AS.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": [],
+      "rotation": 0
+    },
+    {
+      "id": "aPhbdhXuFt",
+      "type": "image",
+      "x": 321,
+      "y": 1710.5,
+      "content": { "type": "external", "content": "/game_assets/JC.jpg" },
+      "backContent": {
+        "type": "external",
+        "content": "/game_assets/Red_back.jpg"
+      },
+      "actions": [
+        { "name": "flip" },
+        { "name": "tap" },
+        { "name": "stack" },
+        { "name": "shuffle" }
+      ],
+      "flipped": false,
+      "unflippedFor": null,
+      "rotation": 0
+    },
+    {
+      "id": "rdmKGYgYqX",
+      "type": "note",
+      "x": -192.5,
+      "y": 1536.5,
+      "value": "- Cliquez pour sélectionner une carte\n- Double-cliquez pour la retourner\n- Essayez une autre action du menu\n- Essayez les raccourcis clavier\n- Sélectionnez toutes les cartes et testez les nouvelles actions\n\n",
+      "label": "Testez-le",
+      "width": "331.0",
+      "height": "255.2",
+      "color": "rgba(184, 233, 134, 1)",
+      "locked": true
+    },
+    {
+      "id": "GzxEj35Jy6",
+      "layer": -1,
+      "type": "zone",
+      "x": -261,
+      "y": 813.5,
+      "label": "Interaction avec les éléments",
+      "width": "730.4",
+      "height": "1116.9",
+      "labelPosition": "top",
+      "locked": true
+    },
+    {
+      "id": "nsYMqz673q",
+      "type": "note",
+      "x": -193.5,
+      "y": 1302.5,
+      "value": "- La dernière icône du menu d'action permet d'afficher le panneau d'édition de l'élément\n- Vous pouvez ainsi modifier tous ses aspects\n- Certaines actions ne sont disponibles que quand ce panneau est affiché\n- Vous ne devriez avoir besoin de ce panneau que rarement",
+      "label": "Modification des éléments",
+      "width": "611.3",
+      "height": "154.8",
+      "locked": true
+    }
+  ],
+  "board": {
+    "size": 2000,
+    "scale": 1,
+    "defaultName": "How to play?",
+    "bgType": "default",
+    "playerCount": [1, 9],
+    "duration": [],
+    "gridSize": 1,
+    "imageUrl": "/game_assets/default.png",
+    "keepTitle": true,
+    "initialBoardPosition": {
+      "top": 24700,
+      "left": 24750,
+      "width": 1000,
+      "height": 300
+    },
+    "defaultLanguage": "en",
+    "materialLanguage": "Multi-lang",
+    "defaultBaseline": "Learn how to play with Airboardgame",
+    "defaultDescription": "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    "translations": [
+      {
+        "language": "fr",
+        "name": "Comment jouer ?",
+        "baseline": "Apprenez à jouer avec Airboardgame",
+        "description": "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles."
+      }
+    ],
+    "published": true
+  },
+  "availableItems": [],
+  "messages": [],
+  "timestamp": 1654541565654,
+  "gameId": "demo"
+}

+ 4 - 2
src/games/perfGame.js

@@ -26,10 +26,11 @@ const genGame = () => {
       scale: 0.5,
       name: "Perf Game",
       published: true,
+      keepTitle: true,
       translations: [
         {
           language: "fr",
-          name: "1 Jeu test de performances et des extrèmes",
+          name: "1 Jeu test de performances et des extrêmes",
           baseline:
             "Un jeu pour tester les performances mais également les limites des différentes saisies. Le texte est tellement long qu'il doit être caché au final.",
         },
@@ -42,9 +43,10 @@ const genGame = () => {
       materialLanguage: "Multi-lang",
       minAge: "10",
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
     },
+    id: "perf",
   };
 };
 

+ 3 - 1
src/games/testGame.js

@@ -299,6 +299,7 @@ const genGame = () => {
       scale: 1,
       name: "Test Game",
       published: true,
+      keepTitle: true,
       translations: [
         {
           language: "fr",
@@ -315,9 +316,10 @@ const genGame = () => {
       materialLanguage: "Multi-lang",
       minAge: "10",
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
     },
+    id: "test",
   };
 };
 

+ 6 - 4
src/games/unpublishedGame.js

@@ -26,23 +26,25 @@ const genGame = () => {
       scale: 0.5,
       name: "Unpublished Game",
       published: false,
+      keepTitle: true,
       translations: [
         {
           language: "fr",
-          name: "2 Jeu non-publie",
-          description: "Un jeu non-publie pour tester",
+          name: "2 Jeu non publié",
+          description: "Un jeu non publié pour tester",
         },
       ],
       playerCount: [1, 9],
       defaultName: "2 Unpublished Game",
       defaultLanguage: "en",
-      defaultDescription: "A non-published game",
+      defaultDescription: "A non published game",
       materialLanguage: "Multi-lang",
       minAge: "10",
       duration: [30, 90],
-      imageUrl: "/game_assets/testgame.png",
+      imageUrl: "/game_assets/default.png",
       gridSize: 1,
     },
+    id: "unpublished",
   };
 };
 

+ 20 - 4
src/hooks/useSession.jsx

@@ -5,19 +5,25 @@ import {
   useBoardConfig,
   useWire,
 } from "react-sync-board";
+import { useTranslation } from "react-i18next";
 
 import SubscribeSessionEvents from "./SubscribeSessionEvents";
 
 import { updateSession, getSession, getGame } from "../utils/api";
 
+import demoEn from "../games/demo_en.json?url";
+import demoFr from "../games/demo_fr.json?url";
+
 export const SessionContext = React.createContext({});
 
+const demos = {
+  fr: demoFr,
+};
+
 const emtpyBoard = {
   items: [],
   availableItems: [],
   board: {
-    size: 1000,
-    scale: 1,
     translations: [
       {
         language: "fr",
@@ -33,6 +39,7 @@ const emtpyBoard = {
 };
 
 export const SessionProvider = ({ sessionId, fromGameId, children }) => {
+  const { i18n } = useTranslation();
   const { setItemList, getItemList } = useItemActions();
   const { messages, setMessages } = useMessage();
   const [availableItems, setAvailableItems] = React.useState([]);
@@ -61,14 +68,23 @@ export const SessionProvider = ({ sessionId, fromGameId, children }) => {
     } catch {
       if (fromGameId) {
         // Then from initial game
-        sessionData = await getGame(fromGameId);
+        if (fromGameId === "demo") {
+          const foundLang = i18n.languages.find((lang) => demos[lang]);
+          if (foundLang) {
+            sessionData = await (await fetch(demos[foundLang])).json();
+          } else {
+            sessionData = await (await fetch(demoEn)).json();
+          }
+        } else {
+          sessionData = await getGame(fromGameId);
+        }
       } else {
         // Empty board
         sessionData = emtpyBoard;
       }
     }
     return sessionData;
-  }, [fromGameId, sessionId]);
+  }, [fromGameId, i18n.languages, sessionId]);
 
   const setSession = React.useCallback(
     async (newData, sync = false) => {

+ 6 - 1
src/i18n/en.json

@@ -295,5 +295,10 @@
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.",
   "Family": "Family",
   "Optional - Use the same family name for items that are part of the same set.": "Optional - Use the same family name for items that are part of the same set.",
-  "Anchor": "Anchor"
+  "Anchor": "Anchor",
+  "Label position": "Label position",
+  "Left": "Left",
+  "Top": "Top",
+  "Keep title": "Show title",
+  "Check it to keep the game title above the game image.": "Check it to keep the game title above the game image."
 }

+ 8 - 3
src/i18n/fr.json

@@ -74,7 +74,7 @@
   "Game deleted": "Jeu supprimé",
   "Game information": "Informations sur le jeu",
   "Game name": "Nom du jeu",
-  "Game saved": "Partie sauvegardée",
+  "Game saved": "Sauvegarde effectuée",
   "Game studio": "Créer",
   "Game": "Jeu",
   "Generating export": "Génération de l'export",
@@ -92,7 +92,7 @@
   "Hide": "Cacher",
   "Horizontal hexagons": "Hexagones horizontaux",
   "I want to play...": "Je veux jouer...",
-  "If you have checked the publish checkbox your game will be public.": "Si vous avait coché l'option « Publier », votre jeu sera visibile publiquement.",
+  "If you have checked the publish checkbox your game will be public.": "Si vous avait coché l'option « Publier », votre jeu sera visible publiquement.",
   "Image": "Image",
   "In progress...": "Demande en cours…",
   "Informations": "Informations",
@@ -295,5 +295,10 @@
   "Let this field empty to snap all items, or set a comma separated list of families that will be snapped.": "Laissez ce champ vide pour aimanter tous les éléments ou définissez une liste de familles d'élément à aimanter séparées par une virgule.",
   "Family": "Famille",
   "Optional - Use the same family name for items that are part of the same set.": "Optionnel : définissez un nom de famille aux éléments appartenant à un même ensemble",
-  "Anchor": "Ancre"
+  "Anchor": "Ancre",
+  "Label position": "Position du label",
+  "Left": "Gauche",
+  "Top": "Haut",
+  "Keep title": "Afficher le titre",
+  "Check it to keep the game title above the game image.": "Cochez pour conserver le titre sur l'image du jeu."
 }

+ 26 - 1
src/ui/formUtils/ColorPicker.jsx

@@ -7,6 +7,25 @@ import styled from "styled-components";
 
 import backgroundGrid from "../../media/images/background-grid.png";
 
+const defaultColors = [
+  "#FFF1E8",
+  "#FFEC27",
+  "#29ADFF",
+  "#FFCCAA",
+  "#00E436",
+  "#C2C3C7",
+  "#FFA300",
+  "#FF77A8",
+  "#83769C",
+  "#FF004D",
+  "#008751",
+  "#AB5236",
+  "#5F574F",
+  "#1D2B53",
+  "#7E2553",
+  "#000000 ",
+];
+
 const Color = styled.div`
   position: relative;
   background-image: url(${backgroundGrid});
@@ -35,7 +54,12 @@ const ColorPickerWrapper = styled.div`
   flex-direction: column;
 `;
 
-const ColorPicker = ({ value, onChange, disableAlpha = true }) => {
+const ColorPicker = ({
+  value,
+  onChange,
+  disableAlpha = true,
+  colors = defaultColors,
+}) => {
   const [showPicker, setShowPicker] = React.useState(false);
   const [currentColor, setCurrentColor] = React.useState(() => {
     if (value === "") {
@@ -83,6 +107,7 @@ const ColorPicker = ({ value, onChange, disableAlpha = true }) => {
             onChange={handleChange}
             disableAlpha={disableAlpha}
             onChangeComplete={handleChangeComplete}
+            presetColors={colors}
           />
           <button onClick={handleClick}>{t("Close")}</button>
         </ColorPickerWrapper>

+ 33 - 30
src/utils/api.js

@@ -90,6 +90,35 @@ export const getBestTranslationFromConfig = (
   return translationsMap[defaultLanguage || "en"];
 };
 
+const demoGame = {
+  id: "demo",
+  owner: "nobody",
+  board: {
+    published: true,
+    defaultName: "How to play?",
+    bgType: "default",
+    playerCount: [],
+    duration: [],
+    gridSize: 1,
+    defaultLanguage: "en",
+    materialLanguage: "Multi-lang",
+    defaultBaseline: "Learn how to play with Airboardgame",
+    imageUrl: "/game_assets/default.png",
+    keepTitle: true,
+    defaultDescription:
+      "# Demo game\n\nThis is a demo game to learn how to play with Airboardgame.\n\nFor other games, you can find useful information about the game like the creator name or the rules.",
+    translations: [
+      {
+        language: "fr",
+        name: "Comment jouer ?",
+        baseline: "Apprenez à jouer avec Airboardgame",
+        description:
+          "# Démonstration\n\nCe jeu vous permet d'apprendre à jouer avec Airboardgame.\n\nPour les autres jeux, vous trouverez dans cette section différentes choses utiles comme le nom de l'auteur ou les règles.",
+      },
+    ],
+  },
+};
+
 export const getGames = async () => {
   const fetchParams = new URLSearchParams({
     fields: "_id,board,owner",
@@ -106,44 +135,18 @@ export const getGames = async () => {
     const serverGames = await result.json();
 
     gameList = serverGames.map((game) => ({
-      name: game.board.defaultName || game.board.name,
       id: game._id,
       owner: game.owner,
-      ...game.board,
       board: game.board,
       url: `${gameURI}/${game._id}`,
     }));
   }
   if (!IS_PRODUCTION || import.meta.env.VITE_CI) {
-    gameList = [
-      {
-        ...testGame,
-        name: "Test Game",
-        data: testGame,
-        id: "test",
-        published: true,
-        ...testGame.board,
-      },
-      {
-        ...perfGame,
-        name: "Perf Test",
-        data: perfGame,
-        id: "perf",
-        published: true,
-        ...perfGame.board,
-      },
-      {
-        ...unpublishedGame,
-        name: "Unpublished Game",
-        data: unpublishedGame,
-        id: "unpublished",
-        published: false,
-        ...unpublishedGame.board,
-      },
-      ...gameList,
-    ];
+    gameList = [testGame, perfGame, unpublishedGame, ...gameList];
   }
 
+  gameList = [demoGame, ...gameList];
+
   return gameList;
 };
 
@@ -195,7 +198,7 @@ export const createGame = async (data) => {
 
 export const updateGame = async (gameId, data) => {
   // fake games
-  if (["test", "perf", "unpublished"].includes(gameId)) {
+  if (["test", "perf", "unpublished", "demo"].includes(gameId)) {
     return data;
   }
   const result = await fetch(`${gameURI}/${gameId}`, {

+ 13 - 0
src/views/BoardView/BoardForm.jsx

@@ -188,6 +188,19 @@ const BoardConfigForm = () => {
         </Label>
 
         <Label>
+          {t("Keep title")}
+          <Field
+            name="keepTitle"
+            component="input"
+            type="checkbox"
+            initialValue={boardConfig.keepTitle}
+          />
+          <Hint>
+            {t("Check it to keep the game title above the game image.")}
+          </Hint>
+        </Label>
+
+        <Label>
           {t("Baseline")}
           <Field
             name="defaultBaseline"

+ 3 - 3
src/views/BoardView/ChangeGameModal.jsx

@@ -23,16 +23,16 @@ const ChangeGameModalContent = ({ onLoad }) => {
 
   const { isLoading, data: gameList } = useQuery("games", async () =>
     (await getGames())
-      .filter((game) => game.published)
+      .filter((game) => game.board.published)
       .sort((a, b) => {
         const [nameA, nameB] = [
           a.board.defaultName || a.board.name,
           b.board.defaultName || b.board.name,
         ];
-        if (nameA < nameB) {
+        if (nameA < nameB || a.id === "demo") {
           return -1;
         }
-        if (nameA > nameB) {
+        if (nameA > nameB || b.id === "demo") {
           return 1;
         }
         return 0;

+ 25 - 20
src/views/GameListItem.jsx

@@ -12,14 +12,14 @@ const Game = styled.li`
   position: relative;
   padding: 0em;
   margin: 0px;
-  min-width: 0; /* Fix for elipsis */
+  min-width: 0; /* Fix for ellipsis */
 
   & .game-name {
     max-width: 80%;
     line-height: 1.1em;
     overflow: hidden;
     margin-bottom: 3px;
-    margin: 0.2em 0 0.5em 0;
+    margin: 0.1em 0 0.1em 0;
     font-size: 2.2vw;
     white-space: nowrap;
     overflow: hidden;
@@ -77,10 +77,7 @@ const Game = styled.li`
       ${({ other }) => (!other ? "" : "border: 1px solid red")};
 
       position: absolute;
-      top: 0;
-      left: 0;
-      bottom: 0;
-      right: 0;
+      inset: 0;
       overflow: hidden;
       display: block;
       display: flex;
@@ -91,19 +88,24 @@ const Game = styled.li`
         filter: blur(5px);
         background-size: cover;
         position: absolute;
-        top: 0;
-        left: 0;
-        bottom: 0;
-        right: 0;
+        inset: 0;
       }
       & > h2 {
+        position: absolute;
+        width: 100%;
+        top: calc(50%-0.6em);
+        z-index: 200;
+        left: 0;
         text-align: center;
         display: inline;
-        font-size: 3em;
+        font-size: 2em;
         white-space: nowrap;
         overflow: hidden;
         text-overflow: ellipsis;
-        margin: 0 0.5em;
+        margin: 0;
+        padding: 0.2em 0.5em;
+        line-height: 1.2em;
+        background-color: #111111a0;
       }
     }
   }
@@ -165,14 +167,17 @@ const getGameUrl = (id) => `${window.location.origin}/playgame/${id}`;
 
 const GameListItem = ({
   game: {
-    published,
     owner,
     id,
-    minAge,
-    materialLanguage,
-    duration,
-    playerCount,
-    imageUrl,
+    board: {
+      minAge,
+      materialLanguage,
+      duration,
+      playerCount,
+      published,
+      imageUrl,
+      keepTitle,
+    },
   },
   game,
   userId,
@@ -196,7 +201,7 @@ const GameListItem = ({
   const [showImage, setShowImage] = React.useState(Boolean(realImageUrl));
 
   const translation = React.useMemo(
-    () => getBestTranslationFromConfig(game, i18n.languages),
+    () => getBestTranslationFromConfig(game.board, i18n.languages),
     [game, i18n.languages]
   );
 
@@ -312,7 +317,7 @@ const GameListItem = ({
               />
             </>
           )}
-          {!showImage && <h2>{translation.name}</h2>}
+          {(!showImage || keepTitle) && <h2>{translation.name}</h2>}
         </span>
       </a>
       <span className="extra-actions">

+ 12 - 9
src/views/GameListView.jsx

@@ -174,9 +174,9 @@ const hasAllowedMaterialLanguage = (filterCriteria, game) => {
   const MULTI_LANG_KEYWORD = "Multi-lang";
 
   return (
-    !game.materialLanguage ||
-    game.materialLanguage === MULTI_LANG_KEYWORD ||
-    filterCriteria.languages.includes(game.materialLanguage)
+    !game.board.materialLanguage ||
+    game.board.materialLanguage === MULTI_LANG_KEYWORD ||
+    filterCriteria.languages.includes(game.board.materialLanguage)
   );
 };
 
@@ -193,16 +193,16 @@ const GameListView = () => {
 
   const { isLoading, data: gameList } = useQuery("games", async () =>
     (await getGames())
-      .filter((game) => game.published)
+      .filter((game) => game.board.published)
       .sort((a, b) => {
         const [nameA, nameB] = [
           a.board.defaultName || a.board.name,
           b.board.defaultName || b.board.name,
         ];
-        if (nameA < nameB) {
+        if (nameA < nameB || a.id === "demo") {
           return -1;
         }
-        if (nameA > nameB) {
+        if (nameA > nameB || b.id === "demo") {
           return 1;
         }
         return 0;
@@ -214,9 +214,12 @@ const GameListView = () => {
       return gameList.filter(
         (game) =>
           (filterCriteria.searchTerm === NULL_SEARCH_TERM ||
-            search(filterCriteria.searchTerm, game.defaultName)) &&
-          hasRequestedValues(filterCriteria.nbOfPlayers, game.playerCount) &&
-          hasRequestedValues(filterCriteria.durations, game.duration) &&
+            search(filterCriteria.searchTerm, game.board.defaultName)) &&
+          hasRequestedValues(
+            filterCriteria.nbOfPlayers,
+            game.board.playerCount
+          ) &&
+          hasRequestedValues(filterCriteria.durations, game.board.duration) &&
           hasAllowedMaterialLanguage(filterCriteria, game)
       );
     }

+ 1 - 1
src/views/GameView.jsx

@@ -50,7 +50,7 @@ const adaptItems = (nodes) => {
 const newGameData = {
   items: [],
   availableItems: [],
-  board: { size: 2000, scale: 1 },
+  board: { size: 2000, scale: 1, imageUrl: "/game_assets/default.png" },
 };
 
 export const GameView = ({ create = false }) => {

+ 39 - 22
src/views/SessionRestoreDim.jsx

@@ -1,4 +1,3 @@
-import React from "react";
 import useAsyncEffect from "use-async-effect";
 import { useBoardPosition } from "react-sync-board";
 
@@ -10,37 +9,55 @@ import useLocalStorage from "../hooks/useLocalStorage";
 const MAX_SESSION_DIM_RETENTION = 1000 * 60 * 60 * 24 * 150;
 
 export const SessionRestoreDim = () => {
-  const { sessionLoaded, sessionId } = useSession();
+  const { sessionLoaded, sessionId, getSession } = useSession();
 
   const [sessionDimensions, setSessionDimensions] = useLocalStorage(
     "sessionDimensions",
     {}
   );
-  const { getDim, setDim } = useBoardPosition();
+  const { getDim, setDim, zoomToExtent } = useBoardPosition();
 
   /**
    * Load the previous dimension for this session if exists
    */
-  React.useEffect(() => {
-    if (sessionLoaded) {
-      if (sessionDimensions[sessionId]) {
-        setTimeout(() => {
-          const dim = { ...sessionDimensions[sessionId], timestamp: undefined };
-          setDim(() => dim);
-        }, 500);
-      }
-      const now = Date.now();
+  useAsyncEffect(
+    async (isMounted) => {
+      if (sessionLoaded) {
+        if (sessionDimensions[sessionId]) {
+          const dim = {
+            ...sessionDimensions[sessionId],
+            timestamp: undefined,
+          };
+          setTimeout(() => {
+            if (isMounted) {
+              setDim(() => dim);
+            }
+          }, 500);
+        } else {
+          const session = await getSession();
+          if (!isMounted) return;
+
+          if (session.board.initialBoardPosition) {
+            setTimeout(() => {
+              zoomToExtent(session.board.initialBoardPosition);
+            }, 500);
+          }
+        }
+
+        const now = Date.now();
 
-      const newDim = Object.fromEntries(
-        Object.entries(sessionDimensions).filter(([, { timestamp }]) => {
-          return timestamp && now - timestamp < MAX_SESSION_DIM_RETENTION;
-        })
-      );
-      setSessionDimensions(newDim);
-    }
-    // We want to set dimension only when session is loaded
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [sessionLoaded]);
+        const newDim = Object.fromEntries(
+          Object.entries(sessionDimensions).filter(([, { timestamp }]) => {
+            return timestamp && now - timestamp < MAX_SESSION_DIM_RETENTION;
+          })
+        );
+        setSessionDimensions(newDim);
+      }
+      // We want to set dimension only when session is loaded
+      // eslint-disable-next-line react-hooks/exhaustive-deps
+    },
+    [sessionLoaded]
+  );
 
   /**
    * Save board dimension in localstorage every 2 seconds for next visit