first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s

This commit is contained in:
william
2026-08-21 16:23:49 +02:00
committed by william
commit 3f4ebc4527
248 changed files with 10180 additions and 0 deletions
+748
View File
@@ -0,0 +1,748 @@
# 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)
```bash
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
```bash
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.