Le tutoriel d’aujourd’hui a pour sujet principal l’intégration de code JavaScript, potentiellement complexe, dans un jeu Twine. L’insertion de code JS est une fonctionnalité intéressante mais peu utilisée, car tout le monde ne se débrouille pas forcément très bien en JavaScript. Nous allons ici essayer de comprendre les rudiments et, avec un peu d’huile de coude, faire un prototype de jeu casual comme on en trouve sur le marché du mobile. J’espère que ce tutorial vous permettra d’envisager de créer des petits jeux et autres prototypes qui mêlent l’écriture non-linéaire à d’autres genres de jeu !

Le genre de jeu que j’ai en tête est relativement simple, mais populaire et répandu notamment sur le marché du mobile. Il s’agit d’un jeu casual formé de séquences de gameplay courtes entrecoupées de séquences narratives, potentiellement avec des choix ou de la gestion de ressources. Le jeu Lily’s Garden en est un parfait exemple, mais il y en a des dizaines de plus sur mobile ; pour ceux qui préfèrent la Nintendo DS, pensez à Professeur Layton ou Puzzle Quest: Challenge of the Warlords. Et pour gérer ces séquences narratives, vous faites comme vous voulez, mais pour simplifier, je vais tirer des péripéties courtes au hasard et avoir vaguement des effets sur les ressources. Ça veut dire que je vais utiliser des narramiettes, super concept implanté depuis peu sur Twine et qui nous donnera une architecture flexible pour les séquences narratives courtes.

Les narramiettes (storylets en anglais) sont discutées dans l’article que nous y avons consacré, qui comprend aussi un tutoriel !

Le souci, c’est que je (Hugo) ne suis pas très bon en JavaScript. Mais ça n’est pas grave : j’ai eu de l’aide ! Narkhos a réalisé la partie JavaScript de cet article, ainsi que les graphismes. Notons que sa participation au concours 2021, “Kerguelen 1991”, avait déjà des mini-jeux rédigés en JavaScript, intégrés à Ink.

Structure générale

Les narramiettes (storylets en anglais) sont des miettes, c’est à dire des tout petits bouts d’histoire, qui ont un effet sur l’histoire et les ressources du joueur en modifiant les variables du jeu ; on les tire au sort, mais en spécifiant des conditions d’apparition, on peut augmenter leur pertinence et faire avancer l’intrigue. Twine peut les stocker, les gérer, les tirer au sort (en vérifiant les préconditions d’abord) de façon native depuis quelques mois, et cet outil gagne à être connu !

Mon jeu est un jeu où l’on incarne un pirate qui se bagarre contre d’autres pirates. Mais comme je ne veux pas de violence, ce sera des duels d’insultes. … Comment ça, déjà fait ? Bon, ok, alors les combats se feront avec du “pierre-papier-ciseaux”. Pour commencer, on collectera juste des pièces de huit. L’idée est : une narramiette est tirée au sort, fournit une péripétie, puis lance un jeu ; quand le jeu est fini, la péripétie est résolue, et le joueur gagne des ressources. Puis on dit au joueur de revenir demain ou de sortir sa carte de crédit, et le lendemain rebelote. C’est une structure très simple, basique, qui ne casse pas des briques, ultra-cheap, et pas très satisfaisante pour le joueur : parfait pour un prototype à raffiner.

Je suis pas en train de me moquer des jeux casual, hein ! (J’ai 100% Puzzle Quest, personnellement.) Cette boucle de gameplay (“un peu d’histoire, un peu de gameplay, allez reviens demain”) est juste extrêmement répandue et facile à mettre en place ; il y a plein de choses à faire niveau game design, création d’une économie dans le jeu, de l’univers, etc., pour que ça soit un jeu qui plaise au joueur et leur donne envie de revenir. Pour des discussions très intéressantes sur la structure narrative de bons jeux casuals mobiles, voir Trailer Park Boys et Lily’s Garden, et bien sûr l’article d’Emily Short sur le sujet.

Mettons donc en place le code de base pour gérer les narramiettes : un passage pour définir les variables importantes (le jour et les pièces) et la boucle de gameplay:

passage initial:
(set: $jour to 0)
(set: $pieces to 8)
(go-to: "Boucle")
JOUR (print: $jour)
(set: $mietteName to (either: …(open-storylets:))'s name )
(link-goto: "Se réveiller", $mietteName)
troisième passage
(storylet: when $pieces < 1000)
Un bateau ! À l'abordage !
[[Jouons !]]

… et il ne nous reste plus qu’à trouver comment ouvrir le mini-jeu.

J’utilise ici Harlowe, car c’est le format dans lequel nous avons écrit tous les tutoriels Twine 2 jusqu’à présent. Cependant, Harlowe est un format limité, en particulier car il ne permet pas aux auteurs de se servir de son JavaScript interne (pas d’API documentée). Ça limite pas mal ! Quand vous serez prêts à aller plus loin en JS, utilisez Sugarcube (le format d’origine, avec lequel on pouvait aller très loin et pour lequel il existe beaucoup de documentation et de réponses sur les forums) ou Snowman.

Intégration d’un mini-jeu en JavaScript

Au début, je pensais que ça allait être simple : prendre un jeu en JavaScript déjà existant, l’intégrer à Twine, trouver comment les faire discuter entre eux… J’avais même trouvé un jeu chouette sur Github, un clone d’Arkanoid en licence libre. Mais malheureusement, ça n’a pas fonctionné, à cause de la CORS : cross-origin resource sharing, un mécanisme implanté dans les navigateurs modernes qui complique l’importation de fichiers JavaScript de plusieurs origines différentes. (C’est rageant, mais c’est super important pour assurer la sécurité de la navigation Internet, donc on peut pas trop grommeler.) On peut sans doute s’en sortir, mais ça dépassait un peu mes compétences, et ça aurait fait un tutoriel à rallonge.

Donc, on va partir du principe que vous avez un jeu JavaScript qui tient dans un seul fichier, et que vous avez vous-même écrit. L’exemple que nous allons choisir est un jeu de pierre-papier-ciseaux écrit par Narkhos, que vous allez copier dans un passage “jouons !” :

 {
 <div id="rps-game">
 </div>
 }
 <script>
 document.getElementById('rps-game').innerHTML = `
 <img src="images/prepare.png" id="enemy" width="360" 
      height="360" alt="Votre adversaire !" style="image-rendering: pixelated;image-rendering: crisp-edges;display: block" />
 <img src="images/rock.png"width="120" 
      height="120" alt="Choisir pierre"
      style="image-rendering: pixelated;image-rendering: crisp-edges;"
      onClick="play(rock);"
 />
 <img src="images/paper.png" width="120" 
      height="120" alt="Choisir papier"
      style="image-rendering: pixelated;image-rendering: crisp-edges;"
      onClick="play(paper);"
 />
 <img src="images/scissors.png" width="120" 
      height="120" alt="Choisir ciseaux"
      style="image-rendering: pixelated;image-rendering: crisp-edges;"
      onClick="play(scissors);"
 />
 `;
 var gameState = "prepare";
 var score = 0;
 var enemy_score = 0;
 var rock = 0;
 var paper = 1;
 var scissors = 2;
 var moves = ["attack_rock", "attack_paper", "attack_scissors"];
 function setGameState(newState) {
     gameState = newState;
     document.getElementById('enemy').src = 'images/'+newState+'.png';
 }
 function play(playerMove) {
     if (gameState != 'prepare') return;
     var enemyMove = Math.floor(Math.random() * 3);  
     setGameState(moves[enemyMove]);
     // Afficher le résultat au bout de quelques instants
     window.setTimeout(function(){
         if (playerMove == enemyMove) {
             // Match null
             setGameState('prepare');
             } else if (playerMove - 1 == enemyMove
                 || (playerMove == rock && enemyMove == scissors) ) {
                 // Victoire
                 setGameState('win');
                 score ++;
             } else {
                 setGameState('loose');
                 enemy_score ++;
             }
         window.setTimeout(function(){
             setGameState('prepare');
         }, 1000);
     }, 1000);   
 }
 </script>
Code et graphismes par Narkos, et ce pirate vient de couper ma feuille. Aaar !

Si vous ne connaissez pas le JavaScript, pas de panique ! Je vous raconte ce qui se passe : on définit d’abord une zone du document comme étant un bloc rps-game, puis on insère dans ce bloc quatre images (le pirate et les trois boutons, qui exécutent la fonction play quand on clique dessus). Vient alors quelques variables encodant les différents états du jeu, puis une fonction setGameState, qui va changer l’état actuel du jeu et l’image correspondante (qui a le nom de l’état dans le nom du fichier). Vient enfin la fonction play, exécutée après votre choix ; on choisit pierre, papier, ou ciseaux, on affiche l’image correspondante, puis au bout d’une seconde on calcule le résultat (victoire ou défaite du joueur) et on change l’état (et donc l’image) du jeu, en mettant le score à jour.

Et les images, où sont-elles ? Tenez, voici un fichier compressé comprenant tout, les images et le Twine final.

Affecter l’état du jeu

Pour l’instant, on arrive sur le jeu, et il tourne en boucle. Il nous faudrait des conditions de fin ; par exemple, on va dire que le jeu s’arrête quand l’un des joueurs est arrivé à 3. Créez tout d’abord les passages “gagné” et “perdu”, qui font gagner des pièces ou pas, puis qui ajoutent 1 à la variable “jour” et reviennent au passage “Boucle”. Par exemple:

Passage "gagné" :
Vous gagnez votre duel contre le pirate !
(set: $pieces to $pieces+1) (set: $jour to $jour+1)
[[S'endormir|Boucle]]

Puis on va ajouter des liens vers ces passages, et les cacher. Pour cela, rajoutons les liens dans la page…

[[Perdu !|perdu]]\
[[Gagné !|gagné]]\
{
 <div id="rps-game">
(etc.)

… et cachons-les en ajoutant dans le script une commande pour les cacher, qui utilise un peu de JQuery pour sélectionner les éléments :

 <script>
$("tw-link:contains('perdu')").hide();
$("tw-link:contains('gagné')").hide();
document.getElementById('rps-game').innerHTML = `
(etc.)

Il ne reste plus qu’à montrer les liens quand on veut ; le meilleur endroit est au moment où on met à jour l’état du jeu, comme le score. Si le score est 2 pour vous et que vous gagnez, alors c’est gagné ; et de même pour perdu ! Modifions donc la fonction setGameState :

function setGameState(newState) {
    if (newState == 'win' && score == 2) { $("tw-link:contains('gagné')").show(); }
    if (newState == 'loose' && enemy_score == 2) { $("tw-link:contains('perdu')").show(); }
    gameState = newState;
    document.getElementById('enemy').src = 'images/'+newState+'.png';
}

Et c’est bon, nous avons terminé la boucle ! Notre jeu est maintenant jouable, et alterne les passages narratifs et les combats !

La suite ?

Il y a encore pas mal de choses à faire pour ce jeu ! Voici une liste d’idées :

  • Actuellement, le joueur peut continuer à jouer même après l’apparition des liens “Gagné” ou “Perdu” ; il faudrait désactiver les liens quand ces liens de fin de jeu apparaissent. Par exemple, vous pouvez modifier la fonction play pour qu’elle ne fasse rien si le score du joueur, ou de l’autre pirate, est déjà à 3.
  • De même, si le jeu est gagné ou perdu, vous pourriez faire une image spéciale (pirate qui pleure, bannière “gagné !”) qui serait affichée à ce moment-là à la place de l’image usuelle du pirate.
  • Rajoutez des narramiettes ! Vous pouvez les compliquer de deux façons : rajouter des péripéties (choix, etc.) comme une branche normale de Twine ; ou ajouter des conditions plus complexes (par exemple, faire des histoires qui n’arrivent que quand on a peu d’argent, et des histoires qui arrivent quand on est plus riche), en compliquant la condition dans le when.
  • Rajoutez des ressources à gérer : moral de l’équipage, réputation, etc. Ceci permettra de varier les péripéties, et de donner l’occasion à la joueuse de faire des compromis (acheter de la bière pour remonter le moral, augmenter la réputation en faisant régner la terreur sur les eaux au dépens du moral de l’équipage, etc.)
  • Rajoutez des objets : faites un passage “magasin”, que l’on peut atteindre à partir du passage “boucle”, et qui propose l’achat d’objets ; quand un joueur achète un objet, mettez une variable à 1 ; et vous pouvez mettre un test sur cette variable dans le when d’une narramiette, ce qui ferait qu’acheter des objets débloque du contenu ! Par exemple, passer d’une coquille de noix à un joli galion débloquera des aventures…

Il y a plein de choses à faire à partir d’un prototype aussi simple, autant sur le côté Twine que sur le côté JavaScript. Explorez et allez plus loin ! Et parlez-en nous dans les commentaires !