Phase -1 : découpe le moteur de play.html en modules JS + premiers tests JS
templates/play.html était un unique fichier HTML+CSS+JS de 1069 lignes,
tout le moteur de jeu vivant dans UN SEUL <script>, sans aucune
couverture de test sur cette logique (seuls le rendu HTML et la syntaxe
JS étaient vérifiés). La feuille de route à venir (état par joueur,
hasard, clavier/minuteur, position/collision, son — voir le plan) va
justement faire grossir ce moteur : "un fichier = une fonction, un
dossier = une responsabilité" s'applique aussi au JS, pas seulement au
Python — le moment de découper est avant d'ajouter encore plus de code,
pas après.
Découpage en 6 fichiers sous static/js/play/, calqués sur les sections
déjà présentes dans le code (aucune réorganisation de logique, une pure
extraction) : screens.js (affichage d'écran, timeline d'animation),
conditions.js (évaluation des conditions — la partie 100% PURE, sans
DOM, la plus testable), actions.js (exécution des actions), triggers.js
(recherche des nœuds déclencheurs, attache des écouteurs), bindings.js
(résolution des {{champ}}, rafraîchissement des données), flow-engine.js
(parcours du graphe, événements personnalisés).
Zéro nouvel outillage : plusieurs <script src> dans l'ordre, partageant
le même espace global qu'avant (aucun bundler, aucune étape de build).
Les 2 URLs de route dont ces fichiers ont besoin (flow_node_run_data/
run_variable, runtime_payload) ne peuvent plus être injectées par Jinja
directement dans le code (un fichier statique n'est jamais passé par le
moteur de templates) — elles sont maintenant posées une fois dans
window.FORGE_PLAY_URLS par le petit <script> inline restant dans
play.html, qui ne porte plus que les données Jinja (gameData) et
l'amorçage (bindClicks() etc. au chargement).
publish/build_package.py : ajoute static/js/play à la liste des fichiers
copiés dans l'exécutable exporté (le mode jouable en dépend désormais).
Premiers tests JS (static/js/play/__tests__/conditions.test.js, lancés
via `node --test`, zéro nouvelle dépendance npm — decision prise avec
l'utilisateur de commencer par la logique PURE seulement, pas par une
couverture DOM via jsdom) : compareValues, resolveVariablePath,
evaluateConditionClause/Node, exactement la logique que les phases à
venir (opérations mathématiques, condition de collision) vont étendre.
tests/conftest.py : nouveau helper play_js_bundle() (concatène tout
static/js/play/*.js) — 13 tests existants qui vérifiaient la présence de
telle fonction/chaîne dans le HTML de /game/<slug>/play (tout le JS y
était inline avant ce découpage) sont mis à jour pour chercher dans ce
bundle à la place ; les tests qui vérifient un CSS/HTML réellement resté
dans play.html (forgeHighlight, forgeDisabled, #playFrame...) continuent
de chercher dans le HTML.
Vérifié : 215 tests pytest passent (aucune régression comportementale,
juste une réorganisation), 13 tests node:test passent, node --check sur
chacun des 6 nouveaux fichiers. Test manuel recommandé (jeu joué de bout
en bout : navigation, clic, survol, répéteur, condition, animation)
avant de considérer le découpage définitivement sans risque.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a445b72a6e
commit
dbada333d5
@@ -0,0 +1,65 @@
|
||||
// ---------- Moteur du graphe de logique ----------
|
||||
// Extrait de templates/play.html (voir plan de modularisation) :
|
||||
// parcours du graphe (déclencheur → condition → action) et déclenchement
|
||||
// d'un événement personnalisé.
|
||||
|
||||
// Exécute le graphe de logique de l'écran courant à partir d'un nœud
|
||||
// (typiquement un Déclencheur touché par le joueur). Un même point de
|
||||
// sortie peut être relié à PLUSIEURS nœuds suivants (plusieurs actions
|
||||
// pour un même déclencheur, ou plusieurs suites pour une même branche
|
||||
// Vrai/Faux) — tous les fils sortants d'un port sont donc suivis, pas
|
||||
// seulement le premier. Chaque nœud Action produit son effet puis
|
||||
// continue sur son (ou ses) fil de sortie ; chaque nœud Condition évalue
|
||||
// une donnée du jeu et suit le(s) fil(s) "Vrai" ou "Faux" en conséquence.
|
||||
// flowScreenId : l'écran PROPRIÉTAIRE du graphe à exécuter — par défaut
|
||||
// l'écran affiché, mais peut être un écran-MODÈLE (voir findTriggerNodess
|
||||
// dans triggers.js) quand le nœud de départ vient d'un déclencheur posé
|
||||
// dans l'éditeur d'un élément de jeu réutilisable, sur un de ses propres
|
||||
// enfants. Sans ce paramètre, un tel déclencheur était bien retrouvé
|
||||
// (findTriggerNodess cherche dans tous les écrans) mais son graphe
|
||||
// n'existait jamais dans l'écran réellement affiché : rien ne se
|
||||
// déclenchait, silencieusement.
|
||||
function runFlowFrom(nodeId, flowScreenId) {
|
||||
const flow = gameData.flows[String(flowScreenId != null ? flowScreenId : window.currentScreenId)];
|
||||
if (!flow) return;
|
||||
const nodesById = {};
|
||||
flow.nodes.forEach(n => { nodesById[n.id] = n; });
|
||||
|
||||
function nextEdges(fromId, port) {
|
||||
return flow.edges.filter(e => e.from_node_id === fromId && e.from_port === port);
|
||||
}
|
||||
|
||||
function step(currentId) {
|
||||
const node = nodesById[currentId];
|
||||
if (!node) return;
|
||||
if (node.node_type === 'trigger') {
|
||||
nextEdges(node.id, 'out').forEach(edge => step(edge.to_node_id));
|
||||
} else if (node.node_type === 'condition') {
|
||||
const port = evaluateConditionNode(node) ? 'vrai' : 'faux';
|
||||
nextEdges(node.id, port).forEach(edge => step(edge.to_node_id));
|
||||
} else if (node.node_type === 'action') {
|
||||
runActionNode(node).then(() => {
|
||||
nextEdges(node.id, 'out').forEach(edge => step(edge.to_node_id));
|
||||
});
|
||||
}
|
||||
}
|
||||
step(nodeId);
|
||||
}
|
||||
|
||||
// Déclenche un événement personnalisé (voir screens/custom_events/,
|
||||
// action "declencher_evenement") : une pure NOTIFICATION, sans aucun
|
||||
// paramètre — scan GLOBAL de gameData.flows, même principe que
|
||||
// findTriggerNodess()/runScreenShowTriggers() (triggers.js), puisqu'un
|
||||
// écouteur (nœud Déclencheur trigger_event="evenement") peut vivre sur
|
||||
// N'IMPORTE QUELLE autre scène ou modèle, pas seulement l'écran affiché.
|
||||
// C'est à CHAQUE écouteur de décider quoi faire ensuite (via sa propre
|
||||
// suite condition/action, avec ses réglages habituels — cible fixe,
|
||||
// "Ligne cliquée"...), jamais à l'événement lui-même de le dicter.
|
||||
window.dispatchGameEvent = function(eventId) {
|
||||
for (const screenIdKey in gameData.flows) {
|
||||
const flow = gameData.flows[screenIdKey];
|
||||
flow.nodes
|
||||
.filter(function(n){ return n.node_type === 'trigger' && n.trigger_event === 'evenement' && n.trigger_custom_event_id === eventId; })
|
||||
.forEach(function(n) { runFlowFrom(n.id, screenIdKey); });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user