Utiliser Loreline avec PHP
Loreline fournit une bibliothèque PHP compatible avec PHP 8.1 et versions ultérieures. Ce guide montre comment configurer un projet, charger un script .lor, gérer les dialogues, les choix et la fin du script, et servir une histoire interactive sous forme de page web en pur PHP.
Installer la bibliothèque
Téléchargez loreline-php.zip (v0.10.0). L'archive contient :
src/: les classes publiquesLoreline\et l'autoloaderlib/: le runtime Lorelinecomposer.json: les métadonnées du paquet, le dossier fonctionne donc aussi comme dépôt Composer de type path
Copiez le contenu de l'archive dans un dossier loreline/ de votre projet et chargez l'autoloader :
<?php
require __DIR__ . '/loreline/src/autoload.php';
use Loreline\Loreline;
Aucune extension n'est nécessaire : le runtime est du PHP pur.
Charger un script
Utilisez Loreline::parse() pour analyser une chaîne .lor :
$script = Loreline::parse(file_get_contents('story/CoffeeShop.lor'));
Si le script utilise des instructions import, passez son chemin et un callback de fichier pour les résoudre :
$handleFile = function (string $path, callable $provide) {
$provide(is_file($path) ? file_get_contents($path) : null);
};
$script = Loreline::parse(
file_get_contents('story/CoffeeShop.lor'),
'story/CoffeeShop.lor',
$handleFile
);
Une précaution propre à PHP : si vous écrivez de la source Loreline directement dans votre code plutôt que de la charger depuis un fichier, utilisez des guillemets simples ou un heredoc <<<'LOR'. L'interpolation Loreline utilise $name, que les chaînes PHP à guillemets doubles essaieraient d'interpoler elles-mêmes :
$source = <<<'LOR'
beat Start
Alex accueille $customer avec le sourire.
LOR;
Gérer les dialogues
La lecture est pilotée par trois callbacks passés à Loreline::play(). Le callback de dialogue reçoit l'interpréteur, un identifiant de personnage (ou null pour du texte narratif), le texte, les tags éventuels, et une fonction à appeler pour continuer :
$onDialogue = function ($interpreter, $character, $text, $tags, $advance) {
if ($character !== null) {
// Résoudre le nom d'affichage depuis la définition du personnage
$name = $interpreter->getCharacterField($character, 'name') ?? $character;
echo "$name : $text\n";
} else {
// Texte narratif (sans personnage)
echo "$text\n";
}
$advance();
};
Dans une application, vous afficheriez le texte et appelleriez $advance() quand le joueur est prêt à continuer.
Gérer les choix
Le callback de choix reçoit un tableau d'objets ChoiceOption. Chaque option a un champ text et un champ enabled. Appelez la fonction avec l'index de l'option choisie, compté dans le tableau complet, options désactivées comprises :
$onChoice = function ($interpreter, $options, $select) {
$enabled = [];
foreach ($options as $index => $option) {
if ($option->enabled) {
$enabled[] = $index;
echo ' [' . count($enabled) . "] {$option->text}\n";
}
}
$answer = (int) trim(fgets(STDIN));
$select($enabled[$answer - 1]);
};
Gérer la fin du script
Le callback de fin est appelé quand le script atteint sa fin :
$onFinish = function ($interpreter) {
echo "--- Fin ---\n";
};
Une fois les trois callbacks en place, lancez l'histoire :
Loreline::play($script, $onDialogue, $onChoice, $onFinish);
Démarrer depuis un beat spécifique
Par défaut, play() démarre au début du script. Pour démarrer depuis un beat spécifique, passez son nom :
Loreline::play($script, $onDialogue, $onChoice, $onFinish, 'MorningScene');
Options de l'interpréteur
Le dernier argument de play() accepte un tableau d'options. Utilisez-le pour enregistrer des fonctions personnalisées appelables depuis votre histoire :
$options = [
'functions' => [
'roll' => function ($interpreter, $args) {
return rand(1, (int) $args[0]);
},
],
];
Loreline::play($script, $onDialogue, $onChoice, $onFinish, null, $options);
Le tableau d'options accepte aussi une entrée translations ; voir Localisation pour le workflow de traduction complet.
Sauvegarder et restaurer l'état
$interpreter->save() retourne l'état complet de l'interpréteur sous forme de chaîne JSON, et Loreline::resume() démarre une nouvelle lecture à partir de cette valeur :
$saveData = $interpreter->save();
// Plus tard, ou dans un autre processus :
Loreline::resume($script, $onDialogue, $onChoice, $onFinish, $saveData);
Comme il s'agit d'une simple chaîne, une sauvegarde peut aller directement dans une session, un fichier ou une base de données :
$_SESSION['save'] = $interpreter->save();
file_put_contents('save.json', $interpreter->save());
Le format est partagé par toutes les intégrations Loreline : une sauvegarde écrite ici peut être reprise par le runtime JavaScript ou n'importe quelle autre cible, et inversement.
Une sauvegarde prise pendant un callback de dialogue ou de choix capture l'état de façon à ce que la reprise relivre ce même callback : le joueur revoit la même ligne, ou les mêmes options. C'est ce comportement qui fait fonctionner le montage web ci-dessous.
Jouer l'histoire en page web
PHP repart de zéro à chaque requête, une histoire ne peut donc pas simplement continuer à tourner comme dans un script console. Le couple save / restore transforme cette contrainte en un flux simple : exécuter l'histoire jusqu'à ce qu'elle attende un choix, sauvegarder dans la session, et afficher la page. Quand le joueur clique sur une option, reprendre depuis la session, répondre au choix relivré avec l'index cliqué, et continuer jusqu'au choix suivant ou à la fin.
Voici un exemple complet et autonome. D'abord l'histoire, enregistrée dans story.lor :
state
hasCroissant: false
character barista
name: Alex
beat Start
Une odeur de café frais et de viennoiserie chaude flotte dans la salle.
barista: Salut ! Qu'est-ce que je te sers aujourd'hui ?
choice
Commander un double expresso
-> Comptoir
Prendre d'abord un croissant
-> Viennoiseries
beat Viennoiseries
hasCroissant = true
Vous prenez le dernier croissant de la matinée, encore tiède.
-> Comptoir
beat Comptoir
if hasCroissant
barista: Bien vu ! Ce croissant ira très bien avec un flat white.
Vous vous installez au comptoir avec votre café et votre croissant.
else
barista: Un double expresso, tout de suite.
Vous vous installez au comptoir et prenez une première gorgée.
barista: Bonne dégustation ! Autre chose avant le coup de feu du matin ?
choice
Rester un moment à regarder les clients
La clientèle du matin va et vient, et la machine à expresso ronronne sa mélodie habituelle.
Finir votre tasse et sortir
Vous videz votre tasse, saluez Alex, et sortez dans le matin.
Puis l'application entière, dans un seul index.php :
<?php
require __DIR__ . '/loreline/src/autoload.php';
use Loreline\Loreline;
session_start();
$script = Loreline::parse(file_get_contents(__DIR__ . '/story.lor'));
// L'histoire jouée jusqu'ici, lue avant l'exécution pour que le callback de
// fin puisse vider la session sans perdre le transcript de la page finale
$transcript = $_SESSION['transcript'] ?? [];
// Ce que cette requête ajoute
$lines = [];
$options = null;
$finished = false;
// L'index du choix que le joueur vient de cliquer, le cas échéant
$picked = isset($_GET['choice']) ? (int) $_GET['choice'] : null;
$onDialogue = function ($interpreter, $character, $text, $tags, $advance) use (&$lines) {
if ($character !== null) {
$name = $interpreter->getCharacterField($character, 'name') ?? $character;
$lines[] = $name . ' : ' . $text;
} else {
$lines[] = $text;
}
$advance();
};
$onChoice = function ($interpreter, $choiceOptions, $select) use (&$options, &$picked) {
if ($picked !== null) {
// La reprise relivre le choix sur lequel l'histoire attendait :
// on y répond avec l'option cliquée par le joueur, et l'histoire
// continue de façon synchrone à partir d'ici.
$index = $picked;
$picked = null;
$select($index);
} else {
// Rien pour répondre : on sauvegarde, et on attend la prochaine requête.
$options = $choiceOptions;
$_SESSION['save'] = $interpreter->save();
}
};
$onFinish = function ($interpreter) use (&$finished) {
$finished = true;
unset($_SESSION['save'], $_SESSION['transcript']);
};
if (isset($_GET['restart']) || !isset($_SESSION['save'])) {
unset($_SESSION['save'], $_SESSION['transcript']);
$transcript = [];
Loreline::play($script, $onDialogue, $onChoice, $onFinish);
} else {
Loreline::resume($script, $onDialogue, $onChoice, $onFinish, $_SESSION['save']);
}
// Conserver toute l'histoire d'une requête à l'autre
$transcript = array_merge($transcript, $lines);
if (!$finished) {
$_SESSION['transcript'] = $transcript;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Le café</title>
</head>
<body>
<?php foreach ($transcript as $line): ?>
<p><?= htmlspecialchars($line) ?></p>
<?php endforeach ?>
<?php if ($options !== null): ?>
<ul>
<?php foreach ($options as $index => $option): ?>
<?php if ($option->enabled): ?>
<li><a href="?choice=<?= $index ?>"><?= htmlspecialchars($option->text) ?></a></li>
<?php endif ?>
<?php endforeach ?>
</ul>
<?php endif ?>
<?php if ($finished): ?>
<p><em>Fin.</em></p>
<p><a href="?restart=1">Rejouer</a></p>
<?php endif ?>
</body>
</html>
Lancez-le avec le serveur intégré de PHP et ouvrez http://localhost:8080 dans un navigateur :
php -S localhost:8080 index.php
Chaque page affiche l'histoire jouée jusqu'ici et les options en attente sous forme de simples liens. Quelques détails méritent attention : les liens de choix portent l'index de l'option dans le tableau complet, les options désactivées gardent donc une numérotation stable. Le transcript est conservé en session pour que la page montre toujours l'histoire entière, et il est lu avant l'exécution de l'interpréteur parce que le callback de fin vide la session. Enfin, comme chaque requête analyse story.lor à nouveau, les modifications de l'histoire sont prises en compte au clic suivant ; pour de gros scripts en production, vous mettriez en cache le script analysé plutôt que de le réanalyser à chaque requête.
Aller plus loin
Le même motif de session s'étend naturellement : remplacez les liens par des formulaires stylés, stockez les sauvegardes en base de données par utilisateur plutôt qu'en session, ou exposez les callbacks derrière un endpoint JSON et affichez depuis JavaScript. Pour tout ce que le langage lui-même peut faire, direction le Guide d'écriture.