PROJET AUTOBLOG


Marien Fressinaud

Site original : Marien Fressinaud

⇐ retour index

Mise à jour

Mise à jour de la base de données, veuillez patienter...

2020

vendredi 3 janvier 2020 à 10:57

Mon année 2019 aura sans aucun doute été très riche sur le plan personnelle. J’ai notamment :

Mais ce que j’ai fait ne m’intéresse guère, c’est plutôt le ressenti que tout cela me laisse qui fait que je suis content de mon année. Pour l’année 2020, je vais essayer de me recentrer pour approfondir quelques voies empruntées.

Valider que Flus est un projet viable (ou non). Il y a encore du boulot, et parfois beaucoup trop. J’avance pas (à pas) mais jamais aussi vite que ce que j’aimerais, probablement lié à ma capacité à m’éparpiller. Je travaille dessus. Et il faut que je m’améliore en communication et marketing. En tout cas ça m’amuse et je suis rentable pour l’instant, c’est déjà ça de pris.

Continuer de faire du pain (genre, beaucoup). J’ai commencé à en faire après le passage de Thomas à l’appartement, j’ai trouvé ça chouette. Au début vraiment une fois de temps en temps, j’ai finalement pris un rythme soutenu depuis la fermeture de ma boulangerie préférée à Grenoble (désormais à Chambéry). Je me dis que ça ferait une chouette reconversion plus tard.

Continuer d’écrire (genre, pas mal). Pour ce blog, j’aimerais réussir à faire quelque chose de peut-être plus personnel. La liste des titres de mes articles de 2019, dans leur majorité, me laisse une impression un peu dérangeante, comme si je ne parlais jamais de moi dedans ; bizarre pour un blog lié à un site personnel. Je le ressens aussi dans le « ton » que j’emploie, dans les mots que je choisis. J’aimerais me rapprocher d’un ton plus « parlé ». J’ai commencé à travailler dessus mais ne suis pas encore totalement satisfait.

Concevoir une V2 pour Lessy (et la terminer). J’ai laissé trainer ce projet en 2019, mais je continue de l’utiliser quotidiennement. J’ai eu une fulgurance le 1er janvier, et toutes les idées que j’avais pour l’améliorer se sont agencées correctement. Je ne me vois pas « améliorer » la version actuelle, donc ce sera un redéveloppement sur des bases plus simples, dans l’idéal tendant sur du low-tech pour une maintenance minimale une fois que j’aurai terminé. En tout cas la vision que j’ai des fonctionnalités est très claire.

Bon, et puis j’ai décidé d’arrêter de procrastiner pour avoir le temps de faire plus de choses. Ça fait quatre jours que je me suis fait une note mentale qui fait qu’à chaque fois que « j’ai la flemme », je me réponds « bouge-toi » et que ça marche. Vous inquiétez pas, ça va pas durer.

Et bonne année !

Une histoire de pizzas : les étapes

mardi 31 décembre 2019 à 10:39

Après une petite pause, voici le sixième épisode de ma série d’articles retraçant la conception d’un mini-jeu vidéo. Comme d’habitude, vous êtes invité·es à lire les articles précédents pour vous y retrouver.

Aujourd’hui, on va faire simple : on va afficher la liste des étapes que le joueur devra suivre pour avancer dans le jeu. Le gros du travail se fera dans l’article suivant.

Pour ce faire, on va commencer par déclarer les étapes en JavaScript :

// On déclare les étapes dans un objet...
const steps = [
    { id: 'STEP_ROLL_OUT_DOUGH', label: 'Étaler la pâte avec les doigts' },
    { id: 'STEP_GRAB_TOMATO', label: 'Aller chercher la sauce tomate sur l’étagère' },
    { id: 'STEP_SPREAD_TOMATO', label: 'Verser la sauce tomate sur la pizza' },
    { id: 'STEP_GRAB_MOZZA', label: 'Aller chercher la mozzarella dans le frigo' },
    { id: 'STEP_SPREAD_MOZZA', label: 'Disposer la mozzarella sur la sauce tomate' },
    { id: 'STEP_GRAB_PIZZA_1', label: 'Prendre la pizza' },
    { id: 'STEP_PUT_PIZZA_IN_OVEN', label: 'Mettre la pizza au four' },
    { id: 'STEP_BAKE_PIZZA', label: 'Cuire 5 secondes' },
    { id: 'STEP_GRAB_PIZZA_2', label: 'Prendre la pizza' },
    { id: 'STEP_DELIVER_PIZZA', label: 'Emporter la pizza jusqu’au passe-plat' },
    { id: 'STEP_FINISH' },
];

const app = new Vue({
    el: '#app',
    data: {
        size: BOARD_SIZE,
        store: gameStore,
        // ... que l'on passe ensuite à data pour pouvoir y accéder depuis la vue
        steps,
    },

    // ...
});

C’est tout bête, rien de bien méchant ici. Il nous faut désormais afficher cette liste dans le HTML.

<div id="app">
    <div class="steps">
        <div class="steps-title">Recette</div>

        <transition v-for="(step, index) in steps" :key="step.id">
            <div
                v-if="step.label"
                :class="['steps-item', { current: step.id === currentStep }]"
            >
                <div class="steps-item-index">{{ index + 1 }}</div>
                <div class="steps-item-label">{{ step.label }}</div>
            </div>
        </transition>
    </div>

    <!-- ... -->
</div>

Vous remarquerez peut-être que je déclare une class .current pour indiquer quelle est l’étape en cours. Néanmoins, je fais appel à une variable currentStep que nous n’avons pas encore déclarée. Il s’agit d’une valeur qui va évoluer au fil du temps à travers des actions liées aux clics du joueur, c’est-à-dire dans notre système onClickSystem. Cette valeur doit donc être déclarée dans le store du jeu. Retour dans le JavaScript :

let gameStore = [
    /*
        On ajoute une nouvelle entité au store pour mémoriser l'étape
        courante. C'est cette valeur qui va permettre d’avancer dans le jeu
        plus tard.
    */
    { id: 'currentStep', value: 'STEP_ROLL_OUT_DOUGH' },

    // ...
];

const app = new Vue({
    // ...

    computed: {
        // On déclare une donnée "computed" : il s'agit d'une donnée qui
        // est calculée depuis une autre donnée déjà stockée dans data
        // (ici, le store). Il s'agit juste de simplifier le code du
        // template HTML.
        currentStep() {
            return getEntity(this.store, "currentStep").value;
        },
    },
});

Avec ça, on a quasiment terminé pour cette fois-ci, on va juste ajouter un peu de CSS pour rendre le tout un poil plus joli.

/* Du CSS pour afficher le bloc des instructions à gauche de la zone de jeu */
#app {
    display: flex;
    justify-content: center;
}

/*
   Et du CSS pour styler les instructions elles-mêmes. Ironiquement, il y a
   besoin de plus de CSS pour cette partie que pour la zone de jeu.
*/
.steps {
    width: 30%;
    margin-right: 1rem;
    padding: .5rem 1rem;

    font-family: sans-serif;

    background-color: #eee;
    border-radius: .5rem;
}

.steps .steps-title {
    margin-bottom: 1rem;
    padding-bottom: .25rem;

    text-align: center;

    border-bottom: 1px solid #ccc;
}

.steps .steps-item {
    display: flex;
    padding: .25rem .5rem;

    align-items: baseline;

    color: #666;
    font-size: .8rem;

    border: .25rem solid transparent;
}

.steps .steps-item.current {
    color: #000;
    font-weight: bold;

    border-color: #00dbcb;
    border-radius: .5rem;
}

.steps .steps-item .steps-item-index {
    display: flex;
    width: 1.25rem;
    height: 1.25rem;
    margin-right: .25rem;

    flex-shrink: 0;
    align-items: center;
    justify-content: center;

    color: #fff;

    background-color: #666;
    border-radius: 50%;
}

.steps .steps-item.current .steps-item-index {
    color: #000;

    background-color: #00dbcb;
}

Avant de finir cet article, je veux juste pointer une petite différence vis-à-vis du jeu initial. J’ai fait le choix ici d’afficher toutes les instructions pour simplifier légèrement le code et ne pas rentrer dans des détails inutiles d’implémentation. Dans le jeu des Ergogames, seulement deux instructions sont affichées à la fois : l’étape courante et l’étape suivante. Si le détail technique n’est pas important, il l’est cependant d’un point de vue utilisabilité. Cette partie a beaucoup évolué avec les retours des utilisateurs et utilisatrices, notamment pour satisfaire la contrainte de l’écran réduit sur mobile. Ce n’est d’ailleurs même pas moi qui aie fait les derniers changements.

Il ne vous reste plus qu’à tester le résultat du code de cet article ici. Il n’y a rien de plus à faire qu’à regarder puisque nous n’avons pas encore implémenté les actions qui vont permettre d’avancer dans le jeu. Ce sera l’objet du prochain article.

Sortie de Boop! 0.4 à l’arrache

samedi 16 novembre 2019 à 20:15

Je me rends compte ce soir que cela fait un an que j’ai commencé mon générateur de sites statiques, Boop!. Comme je n’ai pas sorti de version depuis un petit moment, je fais donc ça maintenant, au pied levé.

Pour les personnes qui débarquent, il s’agit d’un projet totalement personnel, sans ambition aucune : je fais ça pour m’amuser.

Pour les personnes qui se posent la question : oui je continue de l’utiliser, et même de plus en plus. C’est notamment Boop! qui est utilisé pour mon blog « carnet de flus » ainsi que pour le site indiquant le statut de Flus. À chaque fois, cela m’a permis d’améliorer un peu plus le programme. Petit tour d’horizon des nouveautés.

Le truc qui aurait dû être dans la dernière version – je sais pas pourquoi je n’ai pas sorti une autre version d’ailleurs à l’époque – ce sont plus de variables accessibles dans les templates. J’avais notamment besoin d’afficher les séries sur la page du blog.

Tant qu’on est dans les templates, j’ai ajouté un nouveau mot clé (set) permettant d’assigner des variables. 6 lignes de codes en plus, c’est pas cher payé pour la prise de tête que je me suis ainsi évité.

J’ai également ajouté le support des articles privés (qui n’apparaissent ni dans les flux Atom, ni sur la page principale du blog). C’était fait en 5 lignes, je vois pas pourquoi je me serais privé (*ba-dum tss* 🥁).

Ensuite, la possibilité de modifier le « slug » de la page principale du blog permet notamment d’en faire une page d’accueil, comme sur « carnet de flus ». Cette fois-ci j’ai étonnamment enlevé plus de lignes de code que j’en ai ajoutées, ça fait plaisir.

J’ai activé l’extension Markdown qui permet d’ajouter la coloration syntaxique) dans les articles. J’en avais particulièrement besoin pour ma série « Ergogames, une histoire de pizzas ». Ça aurait été illisible sans ça.

Un truc qui m’aura pris un peu plus de temps (2h pour 50 lignes de code) mais me permet de rester serein, c’est l’ajout d’un système de cache pour les articles. Le site est désormais généré en 0,3 seconde au lieu de 2 secondes avant. 2s ça peut paraître peu, mais c’est énorme lorsque je relis un article et que je le régénère plusieurs fois par minute.

J’ajoute à cela des micro-trucs, comme :

Je parle de « micro-trucs » mais ce n’est vraiment pas des détails pour moi : c’est ce qui fait que je trouve mon expérience utilisateur bien au-dessus de celles que j’ai pu avoir avec d’autres générateurs de sites statiques. Par contre je parle bien de « mon » expérience : c’est totalement pensé par et pour moi. Déjà il va falloir que j’améliore grandement la documentation pour en faire un truc plus accessible à d’autres.

Tout ça pour dire que Boop! 0.4 est sortie, youpi ! 🎉

Une histoire de pizzas : coup de pinceau

lundi 11 novembre 2019 à 18:00

Vous aimez les pizzas ? Vous aimez les ratons laveurs ? Alors voici déjà le cinquième épisode de ma série d’articles retraçant la conception d’un mini-jeu vidéo !

Dans le deuxième article, on ne s’était pas trop embêté pour les graphismes : on avait fait de gros blocs de couleurs moches. Il est toutefois temps de corriger ça en passant un coup de peinture sur le jeu. Notre raton va bientôt ressembler à un raton !

Le premier ingrédient qu’il nous faut, c’est un graphiste ; et surprise, je ne le suis pas. Mes anciennes collègues de uxShadow ont fait appel à Goulven Barron pour réaliser les graphismes du jeu (merci à lui !), je n’avais ainsi plus qu’à les intégrer.

Pour cet article, je me suis donc contenté de récupérer des images du jeu d’origine et de les déposer dans un répertoire assets (qu’on peut traduire par « ressources » en bon françois).

Ensuite, il ne nous reste plus qu’à attacher chaque entité à une image et à l’afficher. Pour cela, on va tout simplement créer un nouveau composant au sein des entités (asset) qui contiendra l’URL vers l’image et ajouter cette dernière au HTML à l’aide d’une balise <img />.

<div id="app">
    <div class="board">
        <!-- ... -->

        <div
          v-for="entity in store"
          :key="entity.id"
          :class="['board-cell-entity', entity.id]"
          :style="entityPosition(entity)"
        >
            <!-- On affiche une image si l'entité possède un composant "asset" -->
            <img
                v-if="entity.asset"
                :src="entity.asset"
                alt=""
                class="entity-asset"
            />

            <!-- Sinon, on continue d'afficher le label -->
            <div v-else-if="entity.label" class="entity-label">
                {{ entity.label }}
            </div>
        </div>

        <!-- ... -->
    </div>
</div>

<script>
    // ...

    let gameStore = [
        // On se contente d'ajouter un composant "asset" à chacune de nos
        // entités. Il s’agit juste d'une URL vers une image.
        {
            id: 'meiko',
            asset: 'assets/meiko-face.svg',
            // ...
        },

        {
            id: 'mozza',
            asset: 'assets/pot-mozzarella.svg',
            // ...
        },
        {
            id: 'tomato',
            asset: 'assets/sauce-tomate.svg',
            // ...
        },
        {
            id: 'dough',
            asset: 'assets/pate-pizza-boule.png',
            // ...
        },

        {
            id: 'oven',
            asset: 'assets/four.svg',
            // ...
        },
        {
            id: 'hatch',
            asset: 'assets/passeplat.svg',
            // ...
        },
        {
            id: 'fridge',
            asset: 'assets/refrigirateur.svg',
            // ...
        },
        {
            id: 'workplan',
            asset: 'assets/plan-travail.svg',
            // ...
        },
        {
            id: 'shelf',
            asset: 'assets/etagere-horizontale.svg',
            // ...
        },
    ];

    // ...
</script>

<style type="text/css">
    /* ... */

    /*
        On vire quasiment tout le CSS concernant les entités (sauf les z-index)
        et on le remplace par... seulement ce max-width pour éviter que les
        images débordent des cases. Vous vous attendiez à plus de CSS ? :)
    */
    .board-cell-entity img {
        max-width: 100%;
    }

    /* Bon OK, on en ajoute un pour que la mozza ne déborde pas trop du frigo */
    .board-cell-entity.mozza img {
        max-width: 85%;
    }

    /* ... */
</style>

Et voilà, en seulement quelques lignes, on a largement transformé notre jeu. On est désormais visuellement très proches du jeu d’origine. Un petit détail toutefois reste à régler : on aimerait bien que Meiko se tourne dans la direction vers laquelle on vient de cliquer. Pour cela on va avoir besoin de 4 images représentant Meiko : une pour chaque direction. La question qui se pose est : « comment changer d’image ? »

Je vais aborder ici deux solutions possibles pour faire cela. Le choix que j’ai fait n’est pas forcément le meilleur, mais il sera justifié. Libre à vous de penser que c’est vraiment n’importe quoi et de préférer l’autre solution.

La première façon de faire est de regrouper toutes les images au sein d’un seul fichier : un « sprite » (ou lutin, c’est Wikipédia qui le dit, j’ai toujours entendu parler que de sprite). Ensuite, il faut afficher seulement une partie de l’image, en fonction d’une classe CSS par exemple. Je ne rentre pas dans le détail de l’implémentation, les méthodes sont de plus sensiblement différentes si vous avez un PNG ou un SVG. Cette technique me posait plusieurs problèmes :

Bref, je suis parti sur une seconde solution plus naïve, moins performante et moins élégante, mais qui me permettait au moins d’avancer sans me poser trop de questions.

La technique est simple. Chaque image se trouve dans un fichier à part, et on les rattache via un composant assets (notez le « s » final, c’est un autre composant !) qui va regrouper ces images, indexées par des directions. Lors de l’affichage, on va calculer dynamiquement l’image à afficher en fonction de la direction vers laquelle Meiko est dirigé. Ça ressemble à ça :

<div id="app">
    <div class="board">
        <!-- ... -->

        <div
          v-for="entity in store"
          :key="entity.id"
          :class="['board-cell-entity', entity.id]"
          :style="entityPosition(entity)"
        >
            <!-- On affiche une image si l'entité possède un composant "asset" -->
            <img
                v-if="entity.asset"
                :src="entity.asset"
                alt=""
                class="entity-asset"
            />

            <!--
                Ou bien un composant "assets", il faut alors calculer
                dynamiquement l'image à afficher.
            -->
            <img
                v-else-if="entity.assets"
                :src="entityAssetImage(entity)"
                alt=""
                class="entity-asset"
            />

            <!-- Sinon, on continue d'afficher le label -->
            <div v-else-if="entity.label" class="entity-label">
                {{ entity.label }}
            </div>
        </div>

        <!-- ... -->
    </div>
</div>

<script>
    // ...

    let gameStore = [
        {
            id: 'meiko',

            // Pour Meiko, on remplace le composant "asset" par "assets". Cela
            // nous permettra d'associer plusieurs images, la bonne sera
            // choisie dynamiquement lors de l'affichage.
            assets: {
                bottom: 'assets/meiko-face.svg',
                right: 'assets/meiko-droite.svg',
                left: 'assets/meiko-gauche.svg',
                top: 'assets/meiko-dos.svg',
            },

            // On ajoute également un composant "direction" à Meiko
            direction: 'bottom',

            // ...
        },

        // ...
    ];

    const app = new Vue({
        // ...

        methods: {
            // ...

            // Cette méthode JS calcule dynamiquement l'image à afficher pour
            // les entités possédant un composant "assets"
            entityAssetImage(entity) {
                if (entity.assets == null || entity.direction == null) {
                    return '';
                }

                // Comme le composant "assets" est indexé par les directions,
                // récupérer la bonne image se fait très facilement. Ce code
                // n’est pas très robuste mais comme le jeu est très simple
                // avec peu de contributeurices, on peut s’en contenter pour
                // l'instant.
                return entity.assets[entity.direction];
            },
        },
    });

    // ...
</script>

La dernière étape consiste à changer effectivement la direction de Meiko en fonction d’où le clic a été effectué. Vous l’aurez compris : on va modifier l’état du store, cela se passe donc au sein de notre système onClickSystem.

function onClickSystem(e, store) {
    // ...

    // Notre code doit s'exécuter avoir vérifié que Meiko peut accéder à la
    // case ciblée, sinon il ne doit pas bouger du tout.
    const meiko = getEntity(store, 'meiko');
    if (!positionIsAccessibleFor(meiko, position)) {
        return store;
    }

    // On veut désormais changer la direction de Meiko : il suffit de
    // comparer sa position à la position de la case ciblée pour savoir
    // vers où Meiko doit se tourner.
    let direction = meiko.direction;
    if (position.x > meiko.position.x) { direction = 'right'; }
    else if (position.x < meiko.position.x) { direction = 'left'; }
    else if (position.y < meiko.position.y) { direction = 'top'; }
    else if (position.y > meiko.position.y) { direction = 'bottom'; }

    let updatedStore = setEntityComponents(store, meiko.id, { direction });

    // On fait bien attention à utiliser le updatedStore à partir d’ici au
    // lieu du store sinon Meiko ne se tournera pas.
    const entitiesAtPosition = searchEntitiesAt(updatedStore, position);
    if (entitiesAtPosition.some((entity) => entity.obstruct)) {
        return updatedStore;
    }

    updatedStore = setEntityComponents(updatedStore, meiko.id, { position });

    return updatedStore;
}

Avec ça on a fini notre coup de peinture sur le jeu. D’ailleurs, on a même terminé de toucher au HTML concernant la zone de jeu elle-même : même pas 50 lignes de code. Vous remarquerez sans doute en testant le jeu (le résultat est ici) que Meiko ne change pas immédiatement de direction : il s’agit du temps de charger l’image la première fois. On corrigera ça plus tard dans un article bonus. Pour l’instant, on a quelque chose de bien plus important à faire : ajouter les étapes ainsi que les actions pour avancer dans le jeu. Ce sera l’objet des deux prochains articles (au moins !)

Une histoire de pizzas : contraindre les mouvements

mercredi 6 novembre 2019 à 09:00

Bienvenue sur le déjà quatrième article retraçant le développement d’un mini-jeu web que j’ai développé pour les Ergogames. Les introductions se suivent et se ressemblent toutes alors commencez par lire les autres articles de la série avant d’aller plus loin si vous voulez comprendre quelque chose.

Bien, on a donc vu dans l’épisode précédent comment mouvoir le petit corps de Meiko. Le problème qui se posait alors est qu’il pouvait se déplacer n’importe où sur le terrain de jeu. Je vous propose donc de voir comment le forcer à se déplacer de case en case, sans monter sur le frigo ni l’étagère (c’est dangereux !!)

Pour ce faire, on touchera uniquement au JavaScript cette fois-ci : tout se passera dans notre système onClickSystem, celui qui prend un clic en entrée et modifie la position de Meiko. On commence par vérifier que le clic a bien été fait sur une case du jeu :

function onClickSystem(e, store) {
    e.preventDefault();

    const x = Math.ceil(e.layerX / CSS_CELL_SIZE) - 1;
    const y = Math.ceil(e.layerY / CSS_CELL_SIZE) - 1;

    const position = { x, y };

    // On vérifie que la position visée correspond bien à une case du jeu.
    // En théorie c'est toujours le cas (souvenez-vous la modification
    // faite en CSS dans l'article précédent), mais c'est bien de le
    // vérifier en JS aussi.
    if (
        position.x < 0 ||
        position.x >= BOARD_SIZE ||
        position.y < 0 ||
        position.y >= BOARD_SIZE
    ) {
        return store;
    }

    // ...
}

Si le clic est en dehors du terrain, on retourne le store tel quel, pas besoin de faire autre chose.

La seconde étape est de vérifier que Meiko a effectivement accès à la case sur laquelle on vient de cliquer. Il faut pour cela qu’il s’agisse d’une case adjacente. Avant de vous montrer le code, il me faut expliquer un peu comment calculer ça.

Pour une entité donnée, il existe un maximum de 4 cases accessibles (j’exclue la case sur laquelle se trouve l’entité, mais un autre choix peut être pertinent). Pour faciliter la représentation, prenez le tableau de 3x3 cases suivant (l’entité est représentée par e et les cases accessibles par les o) :

   0 1 2
0 | |o| |
1 |o|e|o|
2 | |o| |

Notre entité est donc en position x=1, y=1. Si l’on fait la différence de son abscisse avec n’importe quelle abscisse des cases accessibles, la valeur absolue est égale soit à 0, soit à 1 (1 - 0 = 1 ; 1 - 1 = 0 ; 1 - 2 = -1). Le résultat est le même pour les ordonnées, mais on remarque que lorsque la différence des abscisses est égale à 1, la différence des ordonnées est égale à 0 (et inversement). Le cas où les deux différences sont égales à 0 correspond à la case où se trouve l’entité, et les cas où les deux sont égales à 1 correspondent aux cases en diagonales.

Pour savoir si Meiko a le droit d’accéder à la case ciblée, il nous faut donc récupérer les deux positions, faire leur différence en valeur absolue en x et en y et vérifier que la somme fait 1.

function onClickSystem(e, store) {
    // ...

    // On récupère notre entité Meiko. On a besoin de connaître sa position
    // pour vérifier que la case visée est accessible.
    const meiko = getEntity(store, 'meiko');

    // On teste que la position est bien accessible à Meiko grâce à une
    // fonction utilitaire. Si elle ne l'est pas, on retourne le store tel
    // quel.
    if (!positionIsAccessibleFor(meiko, position)) {
        return store;
    }

    // ...
}

function getEntity(store, id) {
    // Ici c'est un bête appel à une fonction JS (find) pour trouver notre
    // entité dans le store (qui est un tableau). Le `|| null` permet de
    // retourner null au lieu de undefined, simple coquetterie de ma part.
    return store.find((entity) => entity.id === id) || null;
}

function positionIsAccessibleFor(entity, position) {
    // Si l'entité passée en paramètre n'a pas de position, elle va
    // forcément pas avoir accès à la position donnée puisqu'elle n'est pas
    // présente dans notre tableau !
    if (entity.position == null) {
        return false;
    }

    // On fait la différence (en valeur absolue) de nos abscisses...
    const diff_x = Math.abs(entity.position.x - position.x);
    // ... de nos ordonnées...
    const diff_y = Math.abs(entity.position.y - position.y);
    // ... et on vérifie que seulement l'une des 2 différences est égale
    // à 1. On aurait aussi pu écrire ça `diff_x + diff_y === 1`, mais je
    // trouve ça un peu moins clair.
    return (diff_x === 1 && diff_y === 0) || (diff_x === 0 && diff_y === 1);
}

Avec ça, Meiko arrête de voler à travers le terrain et est obligé de se déplacer sur une case adjacente. Il reste encore à régler sa manie de monter sur tous les objets de la cuisine. On va faire ça en deux temps.

La première chose à faire est d’ajouter un composant à nos entités stockées dans le store pour indiquer qu’elles ne peuvent pas être franchies. J’ai décidé de nommer celui-ci obstruct : si sa valeur est true, alors l’entité ne peut pas être franchie.

let gameStore = [
    // ...

    {
        id: 'meiko',
        label: 'Meiko',
        size: { width: 1, height: 1 },
        position: { x: 3, y: 2 },
        obstruct: true,
    },

    {
        id: 'oven',
        label: 'Four',
        size: { width: 2, height: 1 },
        position: { x: 1, y: 0 },
        obstruct: true,
    },
    {
        id: 'hatch',
        label: 'Passe-plat',
        size: { width: 1, height: 1 },
        position: { x: 5, y: 0 },
        obstruct: true,
    },
    {
        id: 'fridge',
        label: 'Frigo',
        size: { width: 2, height: 2 },
        position: { x: 0, y: 2 },
        obstruct: true,
    },
    {
        id: 'workplan',
        label: 'Plan de travail',
        size: { width: 1, height: 2 },
        position: { x: 5, y: 2 },
        obstruct: true,
    },
    {
        id: 'shelf',
        label: 'Étagère',
        size: { width: 4, height: 1 },
        position: { x: 2, y: 5 },
        obstruct: true,
    },
];

Ici j’ai déclaré le composant uniquement sur les gros objets ainsi que sur Meiko. Si par exemple la sauce tomate traîne au milieu de la cuisine, Meiko pourra passer au-dessus car je ne lui ai pas ajouté obstruct: true.

La deuxième étape est maintenant de récupérer les entités présentes sur la case visée et de vérifier qu’aucune n’est bloquante.

Avant de rentrer dans le code, une fois n’est pas coutume, on va décortiquer un peu ce qu’on veut faire. En théorie, trouver les entités présentes sur une case est facile : il suffit de sélectionner celles dont la position est égale à celle de la case (c’est-à-dire lorsque les valeurs x et y sont identiques). Mais vous avez sans doute remarqué que nos entités ont une taille qui n’est pas toujours de 1x1 case ? La position d’une entité correspond en fait à son angle haut-gauche. On a donc besoin de récupérer l’ensemble des positions occupées par notre entité, et vérifier qu’au moins une correspond à la case visée. Pour cela il faut faire un petit calcul. Prenons l’entité suivante qui occupe quatre cases :

   0 1
0 |e|e|
1 |e|e|

Sa position est x=0, y=0, sa largeur ainsi que sa hauteur sont de 2 chacune. On veut récupérer les cases suivantes à partir de ces deux informations : x=0, y=0, x=1, y=0, x=0, y=1 et x=1, y=1. Pour cela, il suffit d’itérer sur l’intervalle entre 0 et la largeur exclue (ici, « 2 exclu » est égal à 1) : chaque valeur prise est ajoutée au x de la position initiale et équivaut à une position prise par l’entité. On fait évidemment la même chose avec la hauteur et y.

Ce n’est pas très simple à expliquer avec des mots, donc voici le code de cette dernière partie :

function onClickSystem(e, store) {
    // ...

    // On récupère les entités présentes à la position donnée pour vérifier
    // qu'aucune n'est bloquante et empêche ainsi le déplacement.
    const entitiesAtPosition = searchEntitiesAt(store, position);

    // `some` retournera `true` si au moins une entité possède `obstruct` à
    // `true`.
    if (entitiesAtPosition.some((entity) => entity.obstruct)) {
        return store;
    }

    // Et enfin, on met à jour la position de Meiko dans le store. Le seul
    // changement par rapport à la dernière fois est qu'on utilise
    // `meiko.id` au lieu de `'meiko'`.
    let updatedStore = setEntityComponents(store, meiko.id, { position });

    return updatedStore;
}

function searchEntitiesAt(store, position) {
    // On filtre ici les entités présentes à la position donnée en
    // argument. La difficulté réside dans le fait qu'une entité à une
    // taille et peut donc être présente sur plusieurs positions à la fois.
    return store.filter((entity) => {
        if (entity.position == null) {
            // Pas de position, pas de chocolat
            return false;
        }

        if (entity.size == null) {
            // si l'entité n'a pas de taille, on considère quand même
            // qu'elle a une présence de 1x1. On a juste à tester que les
            // positions sont égales. En pratique dans ce jeu, ce cas de
            // figure ne se présente pas.
            return (
                entity.position.x === position.x &&
                entity.position.y === position.y
            );
        }

        // getCoveredPositionsBy retourne l'ensemble des positions que
        // l'entité occupe, on vérifie juste que l'une de ces positions
        // correspond à la position recherchée.
        return getCoveredPositionsBy(entity).some((coveredPosition) => (
            coveredPosition.x === position.x &&
            coveredPosition.y === position.y
        ));
    });
}

function getCoveredPositionsBy(entity) {
    // Ici on collectionne les ~canards~ les positions prises par l’entité.
    const coveredPositions = [];

    // On itère sur l'intervalle entre 0 et la largeur exclue (ouais bon,
    // le code JS est pas hyper accueillant)
    for (const w of Array(entity.size.width).keys()) {
        // Puis sur l'intervalle entre 0 et la hauteur exclue
        for (const h of Array(entity.size.height).keys()) {
            // On a plus qu’à additionner les valeurs à x et y pour obtenir
            // une position occupée par l'entité
            coveredPositions.push({
                x: entity.position.x + w,
                y: entity.position.y + h,
            });
        }
    }
    return coveredPositions;
}

Et voilà ! Vous pouvez tester (le résultat final est ici), Meiko ne peut plus monter sur le frigo, ni sur le plan de travail. Si vous décidez de tripatouiller un peu le code, vous pourrez vérifier que Meiko peut cependant allègrement marcher sur la Mozzarella si on la laisse trainer au milieu de la cuisine (ce n’est pas le cas dans le jeu initial, mais ça me permettait de mieux illustrer l’utilité des composants).

Bien qu’il n’y ait pas eu « beaucoup » de code dans cet article, celui-ci était sans doute assez dense pour la compréhension. Le prochain devrait traiter d’un aspect un peu plus sympa : l’ajout des images. On pourra ainsi se débarrasser de nos blocs moches dessinés à l’arrache.