|
|
|
@@ -1,748 +1,143 @@
|
|
|
|
|
# Forge Engine — prototype (lot de fonctionnalités n°1)
|
|
|
|
|
# Forge Engine
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
Éditeur no-code de serious games 2D pour la formation professionnelle,
|
|
|
|
|
dans le navigateur. Créé pour un public **non technique** (formateurs,
|
|
|
|
|
RH) : aucune ligne de code, aucun graphe de logique générique — un
|
|
|
|
|
assistant pas à pas pour chaque mécanique de jeu.
|
|
|
|
|
|
|
|
|
|
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`.
|
|
|
|
|
> Voir `.claude/Forge_Engine_Cadrage.pdf` (non versionné, document
|
|
|
|
|
> business interne) pour le positionnement produit complet et l'analyse
|
|
|
|
|
> de marché qui a guidé les choix ci-dessous.
|
|
|
|
|
|
|
|
|
|
## Lancer (pour tester maintenant, en ligne de commande)
|
|
|
|
|
## Lancer le projet
|
|
|
|
|
|
|
|
|
|
```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 que permet Forge Engine aujourd'hui
|
|
|
|
|
|
|
|
|
|
## Ce qui est construit dans ce lot
|
|
|
|
|
Chaque jeu créé est un **RPG 2D** : une ou plusieurs scènes (des décors à
|
|
|
|
|
taille fixe), sur lesquelles on pose des personnages, un fond, et des
|
|
|
|
|
widgets d'interface — tout au glisser-déposer, sans jamais toucher à du
|
|
|
|
|
HTML/CSS/JS.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
- **Personnages** : bibliothèque de sprites prêts à l'emploi (personnages
|
|
|
|
|
Kenney CC0 + animaux CraftPix pour les comptes admin), animés
|
|
|
|
|
automatiquement (idle, marche...). Un rôle — **Joueur** (déplacement au
|
|
|
|
|
clavier, suivi par la caméra), **PNJ** ou **Ennemi** — se choisit
|
|
|
|
|
explicitement par personnage ; un personnage fraîchement posé est
|
|
|
|
|
toujours "PNJ" par défaut.
|
|
|
|
|
- **Collision** (onglet dédié de l'éditeur de scène) : pour chaque objet
|
|
|
|
|
de la scène (jamais le joueur lui-même), un assistant "+ Action"
|
|
|
|
|
construit une règle "déclencheur → action" pièce par pièce (jamais un
|
|
|
|
|
formulaire à plusieurs champs) — à la collision ou dans un périmètre,
|
|
|
|
|
déclencher une quête, ou une interaction au clavier ("Appuie sur ...")
|
|
|
|
|
qui elle-même déclenche une quête. Toute la liste se met à jour en
|
|
|
|
|
AJAX (ajout/suppression d'objet, changement de rôle/nom), sans jamais
|
|
|
|
|
recharger la page.
|
|
|
|
|
- **Quêtes & dialogues** : chaque quête a un titre, un objectif, une
|
|
|
|
|
récompense, un statut et un résultat ; son dialogue se rédige
|
|
|
|
|
séparément (3 colonnes — Nouvelle / En cours / Terminée — pour faire
|
|
|
|
|
varier ce que dit un PNJ selon l'avancement).
|
|
|
|
|
- **Widgets d'interface** : Boîte de dialogue, Boîte à quiz, Score — posés
|
|
|
|
|
comme n'importe quel objet de scène, gérés automatiquement par le
|
|
|
|
|
moteur de jeu (jamais de logique à câbler), avec leur propre style
|
|
|
|
|
(police, couleurs) réglable une fois posés.
|
|
|
|
|
- **Export SCORM 1.2** natif (`routes/publish/export_scorm.py`) : un
|
|
|
|
|
paquet .zip autonome (imsmanifest.xml + runtime hors-ligne), prêt à
|
|
|
|
|
déposer dans un LMS (Moodle, 360Learning...) — score et statut de la
|
|
|
|
|
partie remontent automatiquement.
|
|
|
|
|
|
|
|
|
|
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é).
|
|
|
|
|
### Onboarding
|
|
|
|
|
|
|
|
|
|
**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é.
|
|
|
|
|
Un seul parcours de création à l'inscription : **RPG**. La carte de choix
|
|
|
|
|
(`templates/onboarding/onboarding_new.html`) se retourne pour présenter
|
|
|
|
|
l'argumentaire produit.
|
|
|
|
|
|
|
|
|
|
**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.
|
|
|
|
|
### Ce qui a été délibérément retiré
|
|
|
|
|
|
|
|
|
|
### Sur l'ordre de création avec des relations
|
|
|
|
|
Une première version du moteur exposait aussi un type d'écran "document"
|
|
|
|
|
— un éditeur générique façon no-code (blocs de logique en nœuds, timeline
|
|
|
|
|
d'animation, définitions d'objets/champs/relations façon base de
|
|
|
|
|
données, templates réutilisables). Cette complexité ne correspond à
|
|
|
|
|
aucun besoin du public cible (formateurs non techniques) et a été
|
|
|
|
|
supprimée pour recentrer entièrement le produit sur le jeu 2D — voir
|
|
|
|
|
l'historique Git pour le détail de ce retrait.
|
|
|
|
|
|
|
|
|
|
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é.
|
|
|
|
|
## Ce qui manque encore pour une mise sur le marché (voir le cadrage)
|
|
|
|
|
|
|
|
|
|
### Navigation sans rechargement de page (zéro rechargement)
|
|
|
|
|
Sur les 6 fonctionnalités listées comme bloquantes (condition d'achat) par
|
|
|
|
|
le document de cadrage, seul l'**export SCORM** est construit. Restent à
|
|
|
|
|
faire : support **xAPI**, intégration **LTI 1.3**, **hébergement UE +
|
|
|
|
|
RGPD** (DPA), accessibilité **RGAA/WCAG 2.2 AA**. L'export HTML5
|
|
|
|
|
responsive existe déjà via le mode Jouer.
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
## Structure du code
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
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)
|
|
|
|
|
core/ pièces transverses (instance Flask, filtres Jinja, CSRF, auth)
|
|
|
|
|
filters/ filtres Jinja restants (colname)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
routes/ une route HTTP = un fichier, regroupées par domaine
|
|
|
|
|
auth/ inscription, connexion, 2FA, mot de passe
|
|
|
|
|
onboarding/ parcours de création guidé ("Quel jeu veux-tu créer ?")
|
|
|
|
|
games/ créer/renommer/supprimer un jeu, tableau de bord ("Mes écrans")
|
|
|
|
|
screens/ écrans du jeu (liste, création, renommage, taille de scène...)
|
|
|
|
|
scenes/ objets de scène (personnage/décor/fond/widgets), rôle, collision, nom...
|
|
|
|
|
quests/ quêtes + dialogues
|
|
|
|
|
custom_events/ événements personnalisés (déclenchés/écoutés depuis une scène)
|
|
|
|
|
flow/, flow_blocks/ moteur de logique à nœuds (partagé, utilisé par les règles de collision)
|
|
|
|
|
animations/ clips d'animation (Animate.css + images-clés)
|
|
|
|
|
global_vars/ variables globales du jeu
|
|
|
|
|
publish/ export SCORM
|
|
|
|
|
play/, public_play/ mode jouable
|
|
|
|
|
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)
|
|
|
|
|
db/ couche données bas niveau (SQLite, un fichier .db par jeu)
|
|
|
|
|
games/ cycle de vie d'un jeu, catalogue d'onboarding
|
|
|
|
|
definitions/, rows/ objets de données typés + leurs lignes (utilisés par le flow)
|
|
|
|
|
quests/ quêtes et leur dialogue
|
|
|
|
|
custom_events/, global_vars/, scoring/
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
screens/ couche rendu — un fichier par fonction
|
|
|
|
|
scenes/ objets de scène : ajout, rendu, rôle, collision, commandes, dialogue...
|
|
|
|
|
rendering/ personnage (data/animations/rôle), collision, dialogue, quêtes
|
|
|
|
|
flow/ moteur de logique à nœuds (partagé)
|
|
|
|
|
data_actions/ exécution d'une action de flow (modifier une donnée, un score...)
|
|
|
|
|
animations/, custom_events/, labels/, payload/, screens_repo/
|
|
|
|
|
|
|
|
|
|
templates/ pages HTML (Jinja2) — inchangé
|
|
|
|
|
static/ CSS + JS — inchangé
|
|
|
|
|
projects/ un sous-dossier par jeu créé (généré à l'usage) — inchangé
|
|
|
|
|
templates/ pages HTML (Jinja2)
|
|
|
|
|
static/ CSS + JS (static/js/scenes/ = éditeur de scène, static/js/play/ = moteur de jeu)
|
|
|
|
|
projects/ un sous-dossier par jeu créé (généré à l'usage, non versionné)
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
tests/ suite pytest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
## Tests
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pip install -r requirements-dev.txt
|
|
|
|
|
pytest
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Corrections (retours d'usage)
|
|
|
|
|
## Notes techniques
|
|
|
|
|
|
|
|
|
|
- **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.
|
|
|
|
|
- **Navigation sans rechargement** : la plupart des actions de l'éditeur
|
|
|
|
|
de scène (ajout/suppression d'objet, sélection, changement de rôle/nom,
|
|
|
|
|
règles de collision) passent par des appels `fetch()` dédiés qui ne
|
|
|
|
|
touchent que le fragment de page concerné. La navigation entre pages
|
|
|
|
|
différentes (ex. "Mes écrans" ↔ une scène) passe par `static/pjax.js`
|
|
|
|
|
(interception des clics/formulaires, remplacement de `<main>` +
|
|
|
|
|
`#pageChrome`) plutôt qu'un rechargement complet — toute règle CSS dont
|
|
|
|
|
dépend une page doit donc vivre dans `static/style.css` (chargé
|
|
|
|
|
partout), jamais dans un `<style>` inline propre à cette seule page,
|
|
|
|
|
qui ne serait pas repris lors d'une navigation pjax vers elle.
|
|
|
|
|
- **Serveur de développement en mode `threaded=True`** (`app.py`) : une
|
|
|
|
|
scène charge plusieurs images en parallèle (personnages, fond) — sans
|
|
|
|
|
ce réglage, le serveur Werkzeug mono-thread les sert une par une.
|
|
|
|
|