william 9cc6704e08 Replace the icon widget's plain text field with a visual searchable picker
The properties panel's "Icône" setting was a raw text input expecting a Font Awesome class name typed by hand — not something a non-developer using Forge Engine should have to know. It's now a button showing the current icon that opens a modal with the same searchable icon grid as the add-gallery; picking one replaces the selected element's icon and autosaves immediately, no typing required. The class name is still stored the same way (a hidden input feeding the existing generic attr:class control), so no backend changes were needed.
2026-08-24 19:26:35 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00
2026-08-21 16:23:49 +02:00

Forge Engine — prototype (lot de fonctionnalités n°1)

Outil no-code : créer des jeux, définir leurs objets de données (comme des tables de base de données, avec des champs typés et des relations entre eux), et remplir ces données via des formulaires générés automatiquement.

Python + SQLite + Flask. Chaque jeu créé a sa propre base de données réelle (fichier .db), pas de simulation : définir un objet exécute un vrai CREATE TABLE, remplir le formulaire exécute un vrai INSERT.

Lancer (pour tester maintenant, en ligne de commande)

cd forge-engine
pip install -r requirements.txt
python3 app.py

Le navigateur s'ouvre automatiquement sur http://127.0.0.1:5050.

(Ceci est la version de développement. Comme convenu, une version finale empaquetée — double-clic, sans terminal ni installation — sera produite une fois que toutes les fonctionnalités prévues auront été ajoutées.)

Ce qui est construit dans ce lot

  1. Créer un jeu : bouton sur la page d'accueil → crée projects/<nom-du-jeu>/index.html + index.css + index.js (reliés entre eux), et projects/<nom-du-jeu>/game.db (base SQLite dédiée).

  2. Définir un objet (comme une table de base de données) : nom + liste de champs typés (texte, texte long, nombre entier, nombre décimal, oui/non, date, relation vers un autre objet déjà défini dans ce jeu). Chaque objet défini devient une vraie table SQL (obj_<nom>), avec les bonnes colonnes et, pour les relations, une vraie clé étrangère.

  3. Formulaire généré automatiquement : une fois un objet défini, le moteur lit sa définition et construit lui-même le formulaire de saisie adapté (bon type de champ HTML selon le type choisi, menu déroulant peuplé avec les entrées existantes pour les relations). Valider le formulaire enregistre une vraie ligne dans la table SQL correspondante.

  4. CRUD complet sur les trois niveaux :

    • Jeu : renommer, supprimer (dossier + base de données).
    • Objet (page "✏️ Modifier cet objet") : renommer, ajouter un champ (vrai ALTER TABLE ... ADD COLUMN), retirer un champ (vrai ALTER TABLE ... DROP COLUMN), supprimer l'objet entier (vrai DROP TABLE) — bloqué si un autre objet a une relation vers celui-ci, avec indication de quel(s) objet(s).
    • Donnée (une ligne) : modifier, supprimer — bloqué si une autre ligne pointe vers elle via une relation, avec indication de combien et depuis quel objet.
  5. Écrans de jeu (🖥️ Écrans depuis le tableau de bord d'un jeu) — éditeur façon Elementor/Bricks Builder, pensé pour quelqu'un qui ne connaît ni le HTML ni le CSS :

    • 3 panneaux : à gauche, une grille d'icônes pour ajouter un élément — aucun nom de balise HTML visible nulle part. Au centre, l'écran en très grand (repères Portrait / Paysage / Carré, juste visuels — l'écran réel du joueur reste toujours responsive). À droite, les propriétés de l'élément sélectionné, groupées par thème et repliables comme un accordéon (Position & taille, Contenu, Texte, Disposition, Espacement, Bordure, Actions...) — un seul groupe utile ouvert par défaut, les autres se déplient d'un clic sur leur titre, pour garder le panneau lisible même avec beaucoup de réglages.

    • 20 types d'éléments : Texte, Titre, Bouton, Lien, Image (URL ou fichier envoyé depuis ton ordinateur — l'envoi enregistre aussitôt l'image, sans clic supplémentaire à faire pour la voir apparaître), Vidéo (idem), Champ de formulaire — texte / email / mot de passe, Case à cocher, Bouton radio, Zone de texte, Liste déroulante, Groupe de champs, Liste à puces, Liste numérotée, Tableau, Conteneur / décor, Séparateur, Élément de jeu du catalogue, et le Répéteur de données (voir plus bas).

    • Un réglage = un seul champ adapté : case à cocher pour "gras" ou "obligatoire", curseur pour une taille/un arrondi/un espacement, pastille de couleur, boutons ⬅️/⬛/➡️/☰ pour l'alignement — jamais de nom de propriété CSS à taper. Chaque élément dispose maintenant, en plus de ses réglages propres, d'un socle commun : visibilité (visible / masqué / invisible-mais-garde-sa-place), espacement (marge intérieure et extérieure), bordure (épaisseur, couleur, style), et pour les conteneurs/listes/tableaux, une disposition interne façon flexbox (empilement libre / en ligne / en colonne, avec l'espacement entre éléments). Les textes ont en plus gras, italique, souligné, barré et un choix de police (polices système + quelques Google Fonts).

    • Position & taille visibles et modifiables : 4 champs (X, Y, Largeur, Hauteur, en %) dans le panneau de droite, synchronisés avec le glisser-déposer sur l'écran.

    • Glisser-déposer et redimensionnement à la souris, exactement comme avant : on peut vraiment attraper un élément et le faire glisser sur l'écran (un simple clic sans glisser le sélectionne juste). Sans jamais toucher à un fichier HTML/CSS : tout est stocké en base et généré à la volée.

    • ID unique par élément : chaque élément affiche son identifiant (elt-<numéro>) dans ses propriétés — attribué automatiquement, garanti unique dans le jeu.

    • Enchaînement des écrans : liste ordonnée (premier écran, deuxième écran...), réorganisable (↑ / ↓).

    • 🔀 Logique de la scène (éditeur de flow à nœuds) : la logique d'un écran (ce qui se passe au clic ou à la soumission d'un formulaire) ne se règle plus dans les propriétés de chaque élément, mais dans un panneau dédié, rétractable, en bas de l'éditeur d'écran — un vrai canevas où l'on pose des nœuds et où on les relie par des fils, dans l'esprit de Salesforce Flow ou d'Unity Visual Scripting, mais appliqué directement aux éléments et aux objets de données déjà définis dans le jeu :

      • Déclencheur (contour bleu) : "Au clic sur..." ou "À la soumission de..." un élément de l'écran — c'est le point de départ du graphe.
      • Condition (contour orange) : compare un champ d'une ligne d'un objet à une valeur (égal, différent, supérieur, inférieur...), et propose deux fils de sortie, Vrai et Faux, à relier chacun à la suite du graphe qui doit s'exécuter dans ce cas.
      • Action (contour vert) : les mêmes effets qu'avant — changer d'écran, modifier un élément (visibilité, couleurs, taille), ou modifier une donnée d'un objet (texte, nombre, Vrai/Faux, bascule, +/- un montant). Pour relier deux nœuds : clique le point de sortie du premier (côté droit), puis le point d'entrée du second (côté gauche) — un fil apparaît. Reclique un fil pour le supprimer. Chaque nœud peut être glissé sur le canevas pour organiser le graphe, et supprimé avec sa petite croix. Une action "modifier une donnée" met à jour la vraie base de données du jeu, et tout Répéteur de données affichant cet objet à l'écran se met aussitôt à jour tout seul, sans recharger la page. Plusieurs actions pour un même déclencheur : le point de sortie d'un nœud (Déclencheur, ou une branche Vrai/Faux d'une Condition) peut être relié à PLUSIEURS nœuds suivants — au clic, tous s'exécutent (pas seulement le premier relié).

      Effet "va-et-vient" (état à deux positions) : pour une action "Modifier un élément" sur une couleur, une largeur ou une hauteur, une case à cocher "Va-et-vient" fait apparaître une deuxième valeur — le 1er clic applique la 2de valeur, le 2e clic revient à la 1re, et ainsi de suite indéfiniment (par exemple : couleur de fond blanche au départ, bleue au clic, blanche au reclic...). Cet état est mémorisé uniquement dans la page du joueur (pas en base) : il repart de zéro si l'écran est rechargé.

      Limites connues de cette première version : un nœud ne peut pas être modifié une fois créé (il faut le supprimer et en recréer un avec les bons réglages) ; une condition compare toujours une ligne précise d'un objet choisie à la création du nœud (pas encore "la ligne sur laquelle le joueur vient de cliquer" dans un Répéteur) ; il n'y a pas encore de nœud "ET / OU" pour combiner plusieurs conditions avant un branchement ; l'effet "va-et-vient" ne survit pas à un rechargement de page (pour un état qui doit être sauvegardé, utiliser plutôt une Condition sur un champ Oui/Non d'un objet, combinée à une action "Modifier une donnée" en "Basculer Vrai/Faux").

    • Mise en page de l'éditeur d'écran, revue : la page ne défile plus jamais elle-même — elle occupe exactement la hauteur de la fenêtre. Les trois panneaux (ajout d'éléments, écran, propriétés) s'arrêtent tous au même niveau en bas, et défilent chacun pour soi si leur contenu dépasse (la liste d'éléments, l'écran lui-même en orientation portrait sur un grand écran, etc.). La grille "Ajouter un élément" est désormais repliable (clique son titre) pour libérer de la place. Le panneau "🔀 Logique de la scène", une fois ouvert, partage la hauteur de la fenêtre avec le reste au lieu de s'afficher par-dessus : une poignée juste au-dessus de son titre permet de régler sa hauteur à la souris (glisser vers le haut pour l'agrandir, vers le bas pour le réduire).

    • Répéteur de données : relie un élément à un objet défini dans ce jeu (voir "Définir un objet" plus haut) et affiche automatiquement une ligne par donnée existante, à partir d'un modèle de texte avec des {{nom_du_champ}} — par exemple, un objet "Niveau" avec des champs "nom" et "description" peut s'afficher comme {{nom}} — {{description}} répété pour chaque niveau existant, sans rien recopier à la main. La disposition interne (empilement/ligne/colonne + espacement) s'applique à la liste générée. Réglage "Modèle de ligne" : au lieu du modèle de texte, on peut choisir un élément de jeu du catalogue — chaque ligne est alors affichée avec sa mise en forme complète (voir "Éléments de jeu" ci-dessous) plutôt qu'en texte brut.

    • Imbrication réelle d'éléments : un Conteneur / décor, un Répéteur de données ou un Groupe de champs peut désormais contenir d'autres éléments physiquement — texte, image, bouton, un autre conteneur... posés dedans plutôt qu'à côté. Dans le panneau de droite, un élément de ce type affiche une section "Contenu du conteneur" : la liste de ce qu'il contient déjà, et la même grille d'icônes que "Ajouter un élément" pour y placer un nouvel élément. Comme ces éléments deviennent de vrais enfants dans la page générée, la disposition interne (ligne / colonne), l'espacement intérieur (padding) et la bordure du parent s'appliquent réellement à eux — exactement comme le ferait une vraie mise en page flexbox. Un élément posé à l'intérieur d'un Répéteur de données reçoit lui aussi les {{nom_du_champ}} de la ligne en cours : il se répète donc avec le reste. L'imbrication peut se faire sur plusieurs niveaux (un conteneur dans un conteneur, etc.). Un élément posé à l'intérieur d'un autre garde sa hauteur naturelle (comme sur une vraie page web) au lieu de forcer 100% de la hauteur du parent — sinon un deuxième élément ajouté dans le même conteneur se retrouvait poussé hors de la zone visible et disparaissait.

    • Échelle (zoom) : un nouveau réglage curseur, disponible sur tous les éléments (dans "Disposition"), pour agrandir ou réduire un élément sans toucher à sa largeur/hauteur — utile par exemple pour un effet de zoom au survol ou une icône plus petite que sa zone cliquable.

    • Disposition interne complète (conteneurs, répéteurs, listes, tableaux, groupes de champs) : en plus de ligne/colonne, les variantes inversées (ordre inversé), l'alignement transversal (étirés / au début / centrés / à la fin), la répartition dans le sens de la disposition (au début / au centre / à la fin / espacement égal entre, autour, ou uniforme) et une case "autoriser le retour à la ligne" — de quoi retrouver, avec des mots simples, tout ce qu'une vraie disposition flexbox permet de régler.

    • Taille dans le conteneur (nouveau réglage "Taille dans le conteneur", disponible sur tous les éléments) : deux curseurs Largeur / Hauteur en pixels, à 0 par défaut (comportement automatique — un élément posé à l'intérieur d'un conteneur/répéteur/groupe de champs remplit la largeur disponible et s'ajuste en hauteur à son contenu). Une valeur non nulle impose une taille fixe et n'est plus comprimée par la disposition flex environnante : c'est ce qui permet de redimensionner une image (ou n'importe quel autre élément) posée à l'intérieur d'un conteneur — jusqu'ici impossible.

    • Correction — collision en disposition "ligne" : dans un conteneur réglé sur "Alignés côte à côte (ligne)", chaque élément posé à l'intérieur réclamait par défaut 100% de la largeur du conteneur (comme dans les autres dispositions) — deux éléments côte à côte se retrouvaient donc à se disputer toute la largeur, et la disposition flex les comprimait fortement pour les faire tenir (un des deux, souvent une image, pouvait finir écrasé à quelques pixels de large, voire disparaître visuellement). Par défaut, un élément posé dans une disposition en ligne prend maintenant sa taille naturelle (comme le ferait un élément Elementor/Webflow), ce qui rend aussi immédiatement visibles "Espacement égal entre eux / autour / uniforme" : tant que chaque élément occupait 100% de la largeur, il n'y avait aucun espace restant à répartir entre eux.

    • Image — ajustement dans son cadre (object-fit) : nouveau réglage (Remplir en recadrant / Tout montrer sans déformer / Étirer) qui contrôle comment l'image se comporte quand sa taille réelle ne correspond pas exactement à la taille de son cadre — utile dès qu'une largeur et une hauteur fixes sont données à une image (voir "Taille dans le conteneur" ci-dessus) pour éviter qu'elle soit déformée. "Remplir en recadrant" est désormais la valeur par défaut (avant, une image étirée dans un cadre qui n'avait pas ses proportions était déformée sans recours).

    • Nommer un élément : chaque élément peut recevoir un nom (ex: "Bouton Valider", "Image du héros") dans ses propriétés, pour s'y retrouver dans les listes et dans le choix d'une cible d'action sans avoir à deviner lequel est lequel parmi plusieurs éléments du même type. Sans nom, l'élément garde son libellé de type par défaut (ex: "Image").

    • Sélection directe d'un élément imbriqué : plus besoin de repasser par la liste "Éléments de cet écran" à chaque fois — un clic sur l'écran sélectionne précisément l'élément touché, même s'il est physiquement à l'intérieur d'un conteneur, d'un répéteur ou d'un groupe de champs (le glisser-déposer, lui, continue de déplacer la boîte de plus haut niveau, comme avant).

    • Éléments de jeu (🧩 depuis le tableau de bord) : un catalogue de conteneurs réutilisables (Personnage, Outil, icône de mail...), chacun bâti exactement comme un écran — on ouvre "✏️ Modifier le contenu" et on y imbrique/stylise des éléments avec l'éditeur d'écran normal (c'est en réalité un écran caché, invisible dans la liste des écrans du jeu et en mode jouable). On peut optionnellement le lier à un objet, pour utiliser {{nom_du_champ}} dans son contenu. Une fois construit, il a deux usages :

      • Posé directement sur un écran (bouton "🧩 Élément de jeu" dans "+ Ajouter un élément") : tout son contenu est copié en profondeur en éléments indépendants sur cet écran, modifiables séparément par la suite — comme avant. Limite de cette version : si l'élément est lié à un objet, {{nom_du_champ}} n'est alors résolu par aucune ligne précise et reste affiché tel quel ; ce mode convient surtout aux éléments décoratifs (icônes, outils, décor) sans lien à un objet.
      • Choisi comme "Modèle de ligne" d'un Répéteur de données (réglage du Répéteur, à la place du modèle de texte) : pour chaque ligne de l'objet affiché par le Répéteur, son contenu est réaffiché en direct (jamais copié en base) avec les {{nom_du_champ}} de cette ligne — c'est ainsi qu'un objet "Mail" avec 5 lignes peut s'afficher comme 5 cartes stylisées (icône + sujet + expéditeur...) plutôt que comme du texte brut. Le Répéteur et l'éditeur d'écran n'ont besoin de rien connaître du contenu de l'élément de jeu utilisé : la mise en forme vit entièrement dans le catalogue, réutilisable d'un écran (ou d'un jeu) à l'autre. Supprimer un élément de jeu du catalogue supprime aussi son contenu (l'écran caché) ; c'est bloqué tant qu'au moins un exemplaire est posé directement sur un écran (voir "Suppression protégée" ci-dessous).
    • Mode jouable (▶️ Jouer) : joue réellement l'enchaînement des écrans et déclenche les actions au clic, dans le navigateur, à partir des données enregistrées (y compris les Répéteurs, lus en direct) — aucun fichier généré.

    • Suppression protégée : un écran ciblé par une action, ou un élément de jeu posé sur un écran, ne peuvent pas être supprimés tant que la référence existe.

    • Limite volontaire de cette version : le Tableau et la Liste déroulante se remplissent encore via un champ de texte (une ligne par entrée), pas en y glissant d'autres éléments un par un — l'imbrication réelle (Conteneur / Répéteur de données / Groupe de champs) est, elle, disponible (voir "Imbrication réelle d'éléments" ci-dessus).

    • Corrections : le bouton "Définir un nouvel objet" du tableau de bord a désormais le même style de carte que les autres raccourcis (Écrans, Éléments de jeu, Jouer) ; une erreur database is locked pouvait survenir en supprimant un élément (deux requêtes SQLite concurrentes sur le serveur de développement) — corrigé en activant le mode WAL et un délai d'attente sur les verrous de la base.

    • Confusion "Ajouter un élément" vs "Ajouter DANS ce conteneur" : les deux grilles se ressemblaient à l'identique (mêmes icônes, même mise en page), alors qu'elles ne posent pas l'élément au même endroit — celle de gauche le pose directement SUR L'ÉCRAN, celle de droite (qui n'apparaît que si un conteneur/répéteur/groupe de champs est sélectionné) le pose À L'INTÉRIEUR de cet élément. Cliquer par erreur sur celle de gauche pendant qu'un conteneur est sélectionné posait un nouvel élément par-dessus tout le reste (toujours à la même position par défaut), qui pouvait donner l'impression trompeuse qu'un "élément indésirable" venait d'apparaître et d'en cacher un autre. Corrigé par trois changements : la grille de gauche s'appelle maintenant "Ajouter un élément sur l'écran" (sans ambiguïté) ; celle de droite s'appelle "📥 Ajouter DANS ce conteneur" avec un habillage vert distinct ; et un avertissement apparaît dans le panneau de gauche quand un conteneur est sélectionné, pour rappeler laquelle des deux grilles utiliser. Un nouvel élément posé directement sur l'écran est aussi désormais légèrement décalé par rapport au précédent (au lieu de toujours atterrir exactement à la même position), pour limiter les recouvrements accidentels même en cas d'usage normal.

    • Une couleur de fond apparaissait toute seule en changeant la disposition/l'alignement d'un conteneur, et un élément pouvait "disparaître" : le panneau de propriétés d'un élément envoie TOUS ses réglages dans un seul formulaire — donc enregistrer un changement de disposition envoyait aussi, par exemple, le champ de couleur de fond, même non modifié. Or un <input type="color"> affiche toujours une valeur (la couleur par défaut du réglage, purement indicative, quand rien n'a encore été choisi) — cette valeur d'aperçu était donc écrite "en dur" dans le style à chaque enregistrement, quel que soit le champ réellement modifié, ce qui faisait apparaître un fond qui n'avait jamais été demandé (et, en s'accumulant avec d'autres réglages par défaut, pouvait rendre un élément visuellement méconnaissable). Corrigé à la racine : un réglage n'est désormais écrit dans le style/les attributs que s'il a été explicitement modifié — sa valeur par défaut reste un simple aperçu tant qu'on n'y touche pas. Les réglages dont la valeur par défaut a un effet visuel réellement voulu dès la création (ex. "Ajustement dans son cadre" = cover pour une image, afin qu'elle se recadre proprement une fois redimensionnée plutôt que d'être étirée) sont désormais fixés directement à la pose de l'élément, pas au premier enregistrement du panneau — cela ne dépend donc plus de l'ordre dans lequel les réglages sont modifiés.

Sur l'ordre de création avec des relations

Une relation pointe toujours vers un objet déjà défini. Donc si tu veux un objet "Niveau" avec un champ relation vers "Parcours", il faut créer l'objet "Parcours" en premier (même sans aucune donnée dedans), puis créer "Niveau" et choisir "Parcours" comme objet lié pour son champ relation. C'est l'inverse de l'exemple donné : c'est l'objet visé par la relation qui doit exister avant l'objet qui la porte — pas l'objet qui la porte avant l'objet visé.

Navigation sans rechargement de page (zéro rechargement)

Toute la navigation dans l'application (pas seulement l'éditeur d'écran) passe maintenant par static/pjax.js, une petite couche "PJAX" (dans l'esprit de Turbo/Hotwire) : chaque clic sur un lien interne et chaque soumission de formulaire interne est intercepté, la page suivante est récupérée en arrière-plan (fetch), et seuls le <title>, l'en-tête (fil d'Ariane) et le contenu principal (<main>) sont remplacés — au lieu de laisser le navigateur recharger toute la page. L'URL affichée et le bouton "précédent" du navigateur restent corrects grâce à history.pushState/popstate.

Pourquoi PJAX plutôt qu'une réécriture complète en SPA (React ou autre) : le moteur reste 100% rendu côté serveur en Jinja2, ce qui est le bon choix pour un outil interne piloté par une base SQLite par jeu — une SPA aurait demandé de dupliquer toute la logique d'affichage côté client (une API JSON, un routeur, un state management) pour un gain quasi nul ici, alors que PJAX obtient le même résultat perçu ("zéro rechargement", historique correct, formulaires qui marchent) en ne touchant qu'à une seule couche fine, sans toucher aux routes Flask ni aux templates existants.

Points d'attention si tu ajoutes une page avec un script propre à cette page (dans {% block content %} ou via extra_head) :

  • Ce script est réinjecté et réexécuté à chaque navigation vers cette page (y compris quand on y revient plusieurs fois) : évite let/const au premier niveau du script (une redéclaration lèverait une erreur) — une déclaration de fonction ou une variable var est sans risque.
  • Pour désactiver PJAX sur un lien ou un formulaire précis (rare), ajoute l'attribut data-no-pjax. Les liens target="_blank" (ex. "▶️ Jouer") et les liens de téléchargement (download) sont déjà ignorés automatiquement.
  • Les formulaires protégés par un confirm() JavaScript (ex. suppression) continuent de fonctionner normalement : si l'utilisateur annule la boîte de dialogue, PJAX n'intercepte rien et rien ne se passe.

Éditeur d'écran : tout est automatique (déplacer, redimensionner, régler)

L'éditeur d'écran (templates/screen_edit.html) va plus loin que le PJAX général ci-dessus, parce qu'il s'agit d'un usage bien plus intensif : déplacer/redimensionner un élément à la souris et régler ses propriétés sont des actions qu'on répète en continu pendant qu'on construit un écran, pas des navigations occasionnelles. Deux comportements dédiés, indépendants de pjax.js :

  • Glisser-déposer et redimensionnement ne provoquent plus aucune navigation. Tant que l'élément déplacé/redimensionné reste celui déjà sélectionné, rien n'est rechargé : sa nouvelle position/taille est envoyée en arrière-plan (fetch) et les champs "X/Y/Largeur/Hauteur" du panneau de droite sont mis à jour directement en JavaScript. Cliquer (sans glisser) sur un AUTRE élément pour le sélectionner ne recharge pas la page non plus : seul le bloc central de l'éditeur (#builder3 — la liste d'éléments, le canevas et le panneau de propriétés) est regénéré via une requête en arrière-plan et réinjecté, sans toucher au reste de la page (l'éditeur de logique en bas, par exemple, garde son état).
  • Le panneau de propriétés s'enregistre entièrement tout seul — il n'y a plus de bouton "Enregistrer". Chaque réglage se sauvegarde dès qu'on le change : immédiatement pour une case à cocher, une couleur ou une liste déroulante ; après une courte pause (500 ms) pour un champ texte ou un slider qu'on est en train de glisser, pour éviter d'envoyer une requête à chaque caractère tapé. Un indicateur ("Enregistrement..." / "Enregistré automatiquement ✓") remplace l'ancien bouton. Après chaque sauvegarde, seul le canevas (#canvas) est rafraîchi pour refléter le changement visuellement — le formulaire de propriétés lui-même n'est jamais retouché, pour ne jamais faire perdre le focus ou la position du curseur pendant qu'on tape.

Point d'attention si tu ajoutes un nouveau contrôle ou une nouvelle zone dans ce panneau : toute logique branchée avec addEventListener (pas un attribut onclick/onchange inline) doit être (re)branchée dans initBuilderPanel() — cette fonction est rappelée après chaque changement de sélection, puisque #builder3 est entièrement regénéré à ce moment-là.

Cliquer une ligne de Répéteur (déclencheur + action "Ouvrir la ligne cliquée")

Un Répéteur affiche une ligne par entrée d'un objet de données (ex : la liste des mails d'une boîte de réception), mais toutes ses lignes partagent le même modèle d'éléments — il n'existait donc aucun moyen de dire "au clic sur CETTE ligne précise, ouvre CE mail précis" : un déclencheur ne pouvait viser qu'un élément fixe posé une fois pour toutes sur l'écran.

Ça fonctionne maintenant ainsi :

  • Chaque ligne rendue par un Répéteur porte un attribut data-row-id (le vrai id de la ligne de données, voir screens/rendering/render_repeater.py) — à ne pas confondre avec data-element-id, qui reste l'id du MODÈLE de ligne et se répète à l'identique sur chaque ligne.
  • Dans l'éditeur de logique, choisir le Répéteur lui-même comme "Élément" d'un nœud Déclencheur ("Au clic") fait réagir n'importe laquelle de ses lignes au clic (le clic remonte naturellement jusqu'au conteneur du Répéteur).
  • La nouvelle action "Ouvrir la ligne de Répéteur cliquée" retient quelle ligne a réellement été cliquée (window.lastClickedRowId / window.lastClickedDefinitionId, capturés au clic dans bindClicks() de templates/play.html), affiche l'écran de détail choisi, puis résout tous les {{champ}} restés tels quels sur cet écran (applyOpenRowBindings()) avec les valeurs de CETTE ligne — alors que jusqu'ici {{champ}} ne se résolvait qu'à l'intérieur d'un Répéteur (limitation encore documentée dans les tests pour un élément posé directement depuis le catalogue).
  • Limite connue : si le déclencheur est posé sur un élément à l'INTÉRIEUR du modèle de ligne (ex: juste le titre) plutôt que sur le Répéteur lui-même, data-definition-id n'est pas disponible sur cet élément et l'action ne saura pas résoudre la donnée — pose toujours le déclencheur sur le Répéteur.

Un seul écran qui s'adapte à la partie (filtre de Répéteur + déclencheur "affichage")

Certains jeux (ex: une boîte mail avec plusieurs niveaux) n'ont besoin que d'un SEUL écran de jeu, dont le contenu doit changer selon l'état de la partie (niveau atteint, outils débloqués...) plutôt que d'un écran par niveau à maintenir en synchronisation. Deux briques rendent ça possible :

  • Filtre sur un Répéteur (nouveaux réglages "Filtre" dans ses propriétés) : ne garde que les lignes dont un champ correspond à une valeur — fixe (ex. 3) OU une référence {{NomDeLObjet.nom_du_champ}} qui va lire la valeur ACTUELLE de ce champ sur la ligne la plus récente de cet autre objet (voir screens/rendering/filter_repeater_rows.py). Convention : un objet utilisé comme "état de partie" (ex. un objet "Partie" avec un champ "niveau_courant") ne garde qu'UNE seule ligne, mise à jour en place par des actions "Modifier une donnée" plutôt que d'en créer une nouvelle à chaque fois — la référence prend toujours la ligne la plus récente. Le filtre est réévalué à chaque régénération du HTML du jeu (chargement de /play, et rafraîchissement après toute action "Modifier une donnée"), donc automatiquement à jour.
  • Déclencheur "À l'affichage de l'écran" (nouvel événement, en plus de "Au clic" et "À la soumission") : contrairement aux autres déclencheurs, il ne cible pas un élément précis mais l'ÉCRAN ENTIER, et s'exécute automatiquement — au premier affichage, à chaque retour sur cet écran, ET après toute donnée modifiée pendant qu'on y est déjà (voir runScreenShowTriggers() dans templates/play.html, appelée depuis showScreen() et refreshRuntimeData()). Combiné à un nœud Condition et une action "Modifier un élément → Visibilité", ça permet de cacher/montrer un élément selon l'état de la partie SANS qu'un clic soit nécessaire pour le réévaluer — la limite qui empêchait un outil de réapparaître "débloqué" simplement en revenant sur l'écran. À éviter avec le mode "va-et-vient" d'une action liée : il serait réévalué à chaque affichage et basculerait de façon imprévisible plutôt que de rester stable.

Point d'attention : le filtre de Répéteur ne fait pas de vraie jointure — la référence {{Objet.champ}} prend la ligne la plus récente de l'objet visé, pas "la ligne liée à la ligne courante d'un autre Répéteur" ; pour un état de partie à une seule ligne (le cas d'usage visé), ça suffit.

Jauge liée à une donnée, bornage automatique, onglets, conditions combinées

Quatre briques qui se combinent bien avec le déclencheur "À l'affichage de l'écran" ci-dessus, pour construire un écran qui réagit à l'état de la partie sans code :

  • Widget "Jauge (liée à une donnée)" (nouveau widget, icône 📊) : affiche une barre dont la largeur ET la couleur suivent en direct un champ numérique d'un objet — pas besoin d'ajouter la moindre action, la jauge se relit automatiquement à chaque rafraîchissement des données (chargement de /play, après toute action "Modifier une donnée"). Ses réglages : Objet, Champ, Min/Max (bornes d'affichage — la barre est visuellement clampée à 0%/100% même si la vraie valeur dépasse), Couleur basse/haute (dégradé interpolé linéairement selon la position dans la plage), et une case "Afficher la valeur" pour incruster le nombre au centre de la barre. Lit toujours la ligne la PLUS RÉCENTE de l'objet visé (même convention "état de partie à une seule ligne" que le filtre de Répéteur). Voir screens/rendering/render_jauge.py.
  • Bornage automatique des champs numériques : un champ "Nombre entier" ou "Nombre décimal" peut désormais avoir un Min et/ou un Max réglés à la création de l'objet (ou ajoutés après coup en édition). Toute action "Modifier une donnée" (Augmenter de, Diminuer de, Définir à...) qui ferait sortir la valeur de cette plage est automatiquement RAMENÉE à la borne dépassée, plutôt que de continuer à s'accumuler sans limite — plus besoin de poser une Condition séparée juste pour empêcher une jauge de réputation de dépasser 100 ou de passer sous 0. Voir _clamp_to_field_bounds() dans screens/data_actions/apply_data_action.py.
  • Panneau à onglets / visibilité mutuellement exclusive : nouveau type d'action "Afficher cet élément, masquer tous ses frères (onglets / exclusif)" (activer_onglet). Au lieu de poser une action "Modifier élément → Visibilité" par bouton à masquer (N-1 actions pour N onglets), UNE SEULE action suffit : au clic, elle montre l'élément ciblé et cache automatiquement tous les autres éléments qui partagent le même parent (même conteneur) dans l'écran — exactement le comportement d'un jeu d'onglets ou d'un menu à options exclusives. Voir la fonction runActionNode() (branche 'activer_onglet') dans templates/play.html.
  • Conditions combinées (ET / OU) : un nœud Condition peut désormais tester PLUSIEURS champs à la fois dans un seul nœud, au lieu d'enchaîner plusieurs nœuds Condition pour un ET, ou de dupliquer une action derrière plusieurs nœuds Condition pour un OU. Dans le formulaire d'un nœud Condition, le bouton "+ Ajouter une condition" ajoute une clause supplémentaire (Objet / Ligne / Champ / Condition / Valeur, comme la clause principale) et fait apparaître un sélecteur "Combiner les conditions avec : ET / OU". Exemple : "réputation < 20 OU niveau ≥ 3". Compatibilité : un nœud créé AVANT cette fonctionnalité (ou un nœud à une seule clause) n'a pas de cond_clauses en base et garde exactement son ancien comportement (une seule comparaison) — voir cond_clauses / cond_combinator dans screens/flow/ensure_flow_schema.py et evaluateConditionNode() / evaluateConditionClause() dans templates/play.html.

Survol, séquences temporisées, surbrillance, overlay, verrouillage

Cinq briques de confort, chacune contournable en théorie mais coûteuse en câblage manuel sans elles :

  • Interactions au survol : un nouveau réglage "Texte affiché au survol de la souris", disponible sur TOUS les widgets (via UNIVERSAL_CONTROLS — voir screens/widgets/control_groups/hover_controls.py), échange le texte affiché contre ce texte alternatif tant que la souris survole l'élément, puis le restaure au départ de la souris — exactement le cas d'usage visé ("survoler un nom révèle l'adresse réelle, survoler un lien révèle l'URL réelle"). Laissé vide = aucun changement, donc aucune régression sur les éléments déjà créés. Ignoré volontairement sur les éléments qui ont des enfants (conteneurs), pour ne jamais écraser une mise en page imbriquée. Voir l'attribut data-hover-text posé par render_element_html.py et bindHoverTexts() dans templates/play.html.
  • Séquences temporisées : nouveau type d'action "Attendre quelques secondes avant de continuer" (attendre), qui suspend la SUITE du fil de logique (les nœuds reliés après lui) pendant N secondes avant de continuer — combiné au déclencheur "À l'affichage de l'écran" (voir plus haut), ça permet un mail ou un événement qui "arrive" tout seul quelques secondes après l'ouverture de l'écran, sans action du joueur. Le joueur continue d'interagir normalement avec le reste de l'écran pendant l'attente. Voir la branche 'attendre' de runActionNode() dans templates/play.html.
  • Surbrillance générique dynamique : nouvelle valeur "Surbrillance" pour l'action "Modifier un élément", applicable à N'IMPORTE QUEL élément (pas seulement les boutons) — pose un liseré doré clignotant (.forgeHighlight, animation CSS) pour attirer l'attention du joueur vers ce qu'il doit toucher ensuite (ex. un mentor qui "montre" un bouton), sans avoir à poser une bordure de couleur togglée à la main. Activer / Désactiver / Basculer, comme la Visibilité.
  • Overlay / modale réutilisable : nouveau widget "Superposition / boîte de dialogue" (icône 🪟), prêt à l'emploi — contrairement à tous les autres widgets, sa position ne dépend PAS de l'endroit où il est glissé sur le canevas (position:fixed; inset:0, codé en dur) : il couvre TOUJOURS tout l'écran, avec un voile semi-transparent et une boîte centrée qui contient les éléments posés à l'intérieur (comme un conteneur normal). Démarre MASQUÉ par défaut à sa création (sinon il couvrirait l'écran dès qu'on le pose) — l'ouverture et la fermeture réutilisent l'action existante "Modifier un élément → Visibilité" (rendre visible / masquer), fermeture manuelle uniquement, sans mécanisme de clic-en-dehors-pour-fermer. Voir screens/rendering/render_overlay.py.
  • Verrouillage d'un élément après décision : nouvelle valeur "Désactivé" pour l'action "Modifier un élément", DISTINCTE de "Invisible" — l'élément reste visible mais devient grisé et inerte (.forgeDisabled : opacité réduite + pointer-events:none, qui bloque aussi le déclencheur "Au clic" éventuellement posé dessus). Utile pour un bouton "Répondre" qui reste affiché mais ne doit plus pouvoir être recliqué une fois le mail traité. Activer / Désactiver / Basculer.

Structure

Le code est organisé en un fichier par fonction et un dossier par responsabilité — pensé pour rester modifiable, testable et évolutif à mesure que le moteur grandit, plutôt que quelques gros fichiers monolithiques. app.py reste le point d'entrée (python3 app.py ne change pas), mais il n'est plus qu'un mince assemblage : toute la logique vit dans les dossiers ci-dessous.

app.py                    point d'entrée : assemble core/ + routes/, lance le serveur

core/                      pièces transverses de l'application Flask
  flask_app.py               crée l'instance Flask partagée (app)
  jinja_filters.py            enregistre les filtres Jinja (colname, elstyle, elabel)

filters/                   un fichier par filtre Jinja
  colname_filter.py
  element_style_filter.py

routes/                    un fichier par route HTTP, regroupées par domaine
  games/                      créer/renommer/supprimer un jeu, tableau de bord
  objects/                    définitions d'objet, champs, données (CRUD)
  screens/                    écrans du jeu (liste, création, édition...)
  elements/                   éléments posés sur un écran
  element_types/              catalogue d'éléments de jeu réutilisables
  flow/                       éditeur de logique à nœuds (déclencheur/condition/action)
  legacy_actions/             ancien système d'actions par élément (conservé, non utilisé par l'interface actuelle)
  uploads/                    envoi de fichiers (images/vidéos)
  play/                       mode jouable

db/                        couche données "objets" — un fichier par fonction
  connection.py, slugify.py, table_name_for.py, game_dir.py, db_path.py, constants.py
  games/                      cycle de vie d'un jeu
  definitions/                définitions d'objet et leurs champs
  rows/                       lignes de données (CRUD)

screens/                   couche données + rendu "écrans de jeu" — un fichier par fonction
  widgets/                    catalogue de widgets, réglages ("controls"), groupes de réglages
  rendering/                   construction du HTML réel d'un élément (récursif)
  screens_repo/                 CRUD des écrans
  elements/                      CRUD des éléments posés sur un écran
  element_types/                  catalogue d'éléments de jeu réutilisables (conteneurs imbriqués)
  flow/                            moteur de logique à nœuds
  data_actions/                     exécution d'une action "modifier une donnée"
  legacy_actions/                    ancien système d'actions (conservé)
  labels/                             constantes d'affichage (libellés)
  payload/                             assemble les données du mode jouable

templates/                pages HTML (Jinja2) — inchangé
static/                    CSS + JS — inchangé
projects/                  un sous-dossier par jeu créé (généré à l'usage) — inchangé

tests/                     suite de tests automatisés (pytest)
  conftest.py                fixtures partagées (client Flask de test, jeu de test jetable)
  test_db_layer.py            couche données, sans Flask
  test_screens_and_elements.py  écrans/éléments/éléments de jeu/répéteur, via le client de test
  test_flow.py                 éditeur de logique à nœuds

Chaque import db / import screens continue de fonctionner exactement comme avant (db.connect(...), screens.render_element_html(...), etc.) : db/__init__.py et screens/__init__.py réexportent l'intégralité de l'API publique historique, pour que ce découpage interne reste invisible du reste du moteur — aucune route, aucun template n'a eu besoin de changer.

Lancer les tests

pip install -r requirements-dev.txt
pytest

Corrections (retours d'usage)

  • ID d'élément en champ non modifiable : dans le panneau de propriétés, l'ID de l'élément (elt-…) est maintenant affiché dans un champ texte readonly, plutôt qu'en simple texte.

  • Bouton "+10 points" instable en mode Jouer (corrigé) : un bouton relié à une action "Modifier une donnée" (ex: incrémenter un score) pouvait, après plusieurs clics, appliquer l'effet plusieurs fois d'un coup (+100, -20, ou retomber à 0 au lieu de ±10). Cause : chaque donnée modifiée déclenche un rafraîchissement qui ré-attache les gestionnaires de clic sur toute la scène ; un bouton ordinaire (contrairement à un Répéteur ou une Jauge) garde le même nœud HTML d'un rafraîchissement à l'autre, donc ses gestionnaires de clic s'empilaient au lieu d'être remplacés. Un clic fini par déclencher l'action N fois pour N rafraîchissements passés. Corrigé en marquant chaque élément déjà relié, pour ne l'attacher qu'une seule fois.

  • Textes d'aide allégés : les explications détaillées entre parenthèses affichées un peu partout dans l'éditeur ont été retirées ou raccourcies — sur tous les écrans, pas seulement l'éditeur de scène (nouvelle passe complète sur tous les labels, options de menus déroulants et placeholders).

  • Espacement (gap) inefficace en disposition par défaut, corrigé : le réglage "Espace entre les éléments" n'avait aucun effet tant que la "Disposition interne" restait sur "Empilement libre" — le CSS gap ne fonctionne que sur un conteneur flex/grid, or "Empilement libre" ne posait aucun display:flex. Un conteneur/répéteur/groupe de champs applique maintenant flex + colonne par défaut dès qu'aucune disposition n'a été choisie explicitement (visuellement identique à l'empilement d'avant), donc l'espacement fonctionne dès la création, sans avoir à toucher la disposition au préalable.

  • Marges et bordures réglables par côté : en plus des réglages globaux existants ("Marge intérieure", "Marge extérieure", "Épaisseur de la bordure"), chaque côté (haut/droite/bas/gauche) peut désormais être réglé individuellement — utile par exemple pour une seule bordure basse, ou un padding asymétrique. Les réglages par côté, une fois utilisés, priment sur le réglage global correspondant.

  • Apparence de l'éditeur après suppression d'un élément : supprimer un élément qui a un parent (posé à l'intérieur d'un conteneur/répéteur/groupe de champs) resélectionne maintenant ce parent, au lieu de laisser le panneau de propriétés vide.

  • Timeline d'animation (Animate.css + animations personnalisées) : dans l'éditeur d'écran, un nouveau panneau rétractable "🎬 Timeline d'animation" est positionné sous "🔀 Logique de la scène". Il permet de poser des clips d'animation sur n'importe quel élément de l'écran, sur un axe temporel en secondes (depuis l'affichage de l'écran) :

    • Animations Animate.css : choix parmi le catalogue complet de la bibliothèque (apparitions, rebonds, glissements, zooms, rotations, retournements, etc.), avec durée, délai, easing et nombre de répétitions réglables.
    • Animations personnalisées : un éditeur d'images-clés (keyframes) — chaque image-clé a un pourcentage (0-100) et une liste de propriétés CSS (ex. transform: scale(1.2); opacity: 0.5;), converties en @keyframes CSS au moment de la lecture.
    • Chaque clip se pose et se règle à la souris directement sur la timeline : glisser un clip change son instant de départ, glisser son bord droit change sa durée ; cliquer dessus (sans glisser) ouvre son formulaire d'édition détaillé, avec un bouton "▶ Aperçu" pour voir l'animation directement sur le canevas d'édition.
    • En mode Jouer, chaque clip se déclenche automatiquement à son instant de départ lors de l'affichage de l'écran qui le contient.
  • Échelle/animation d'un élément posé sur l'écran, corrigée : l'échelle ("Disposition" → curseur "Échelle") et les animations de la timeline restaient visuellement "coincées" à l'intérieur de la boîte d'origine de l'élément, comme si l'effet s'appliquait à un contenu emprisonné dans un cadre immobile — c'était bien le cas : ces réglages vivaient sur la balise intérieure, alors que le cadre qui porte réellement la position/taille de l'élément sur l'écran (et son contour de sélection dans l'éditeur) restait fixe autour. L'échelle et les animations visent maintenant ce cadre directement, comme le reste de l'élément — plus de recadrage parasite. Un élément posé À L'INTÉRIEUR d'un conteneur/répéteur/groupe de champs n'a jamais eu ce souci (pas de cadre séparé pour lui).

  • Format d'aperçu (Portrait/Paysage/Carré) mémorisé par écran : ce choix ne vivait qu'en mémoire dans le navigateur — poser ou supprimer un élément, ou sélectionner un autre élément, redemande ce panneau au serveur, qui repartait alors systématiquement sur Portrait. Résultat : l'écran de travail semblait "s'agrandir ou rétrécir" au fil des actions quand on travaillait en Paysage ou en Carré (retour brutal en Portrait à chaque fois). Le format choisi est maintenant enregistré par écran et restauré à chaque fois que ce panneau est réaffiché.

  • Zone de jeu à proportion fixe en mode Jouer, contre la déformation en % : les positions et tailles des éléments sont en pourcentage de l'écran, ce qui les déformait dès que la fenêtre du joueur n'avait pas exactement la même proportion que celle choisie à la conception (ex. un écran conçu en Portrait, joué dans une fenêtre large, étirait/écrasait tout). La zone de jeu garde maintenant toujours la proportion de l'écran affiché (Portrait/Paysage/Carré, par écran), avec des bandes noires (letterboxing) si besoin — comme un lecteur vidéo — pour rester fidèle à ce qui a été conçu, quel que soit l'écran du joueur.

Prochaines fonctionnalités

À ajouter au fur et à mesure, comme convenu.

S
Description
No description provided
Readme
1.6 GiB
Languages
Python 55.5%
JavaScript 35.5%
HTML 6.5%
CSS 2.4%