Commit Graph
87 Commits
Author SHA1 Message Date
williamandClaude Sonnet 5 390ee831ca Quêtes : dialogue dans une modale à part, en plein écran
Build and deploy / test-python (push) Successful in 6m17s
Build and deploy / test-js (push) Successful in 1m14s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Sépare la modale unique en deux étapes distinctes demandées par
l'utilisateur : #questFieldsModal ("je crée" — titre/objectif/
récompense/statut/résultat, taille normale) puis, sur un bouton dédié
"💬 Ajouter les dialogues", #questDialogueModal ("j'ajoute les
dialogues" — les 3 colonnes de répliques SEULES, occupant tout
l'écran). Un bouton "←" ramène à la fiche ; "✓ Utiliser cette quête"
reste disponible dans les deux, pour l'intégration avec l'éditeur de
collision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 20:54:00 +02:00
williamandClaude Sonnet 5 78c1aba8a0 Éditeur de quêtes : onglet à côté de Collision, pas une page à part
Build and deploy / test-python (push) Successful in 6m7s
Build and deploy / test-js (push) Successful in 59s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Corrige la mauvaise interprétation de la demande initiale : la page
dédiée "🗺️ Quêtes" (nav du jeu) est retirée au profit d'un nouvel
onglet "🗺️ Quêtes" DANS l'éditeur de scène 2D, juste à côté de
"🧩 Collision" — même liste tabulaire que l'onglet "📣 Événements"
(titre/objectif/récompense/statut/résultat par ligne, pas des cartes),
un clic sur une ligne ouvrant la modale d'arbre de dialogue déjà
construite. L'assistant "+ Action" de l'éditeur de collision ouvre
toujours cette même modale pour "Déclencher une quête".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 20:46:27 +02:00
williamandClaude Sonnet 5 51427b5297 Éditeur de quêtes : titre/objectif/récompense/statut/résultat + dialogue à 3 colonnes
Build and deploy / test-python (push) Successful in 6m14s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Nouvel espace "🗺️ Quêtes" (game-wide, comme les événements
personnalisés) : liste de cartes + "+ Nouvelle quête", chaque carte
ouvrant une modale d'arbre de dialogue en bulles de théâtre
("speaker: texte"), une colonne par statut de quête (nouvelle/en
cours/terminée) puisque le dialogue joué dépend de l'état de la
quête au moment où le joueur parle au PNJ. Bulles alternées
joueur/PNJ, couleur différente selon qui parle.

L'action "Déclencher une quête" de l'éditeur de collision n'est plus
un champ texte libre : elle ouvre directement CETTE MÊME modale
(picker de quêtes existantes + création à la volée), et finalise la
règle avec l'id numérique de la quête choisie — collision_rules.py
migré en conséquence (quete_id devient un entier, comme evenement_id).

Corrige aussi la miniature d'objet de la carte "🧩 Collision", 3× plus
grande (120px) comme demandé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 20:24:57 +02:00
williamandClaude Sonnet 5 4391b36d45 Fix miniature de carte "🧩 Collision" échappée du cadre
Build and deploy / test-python (push) Successful in 6m46s
Build and deploy / test-js (push) Successful in 1m3s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
render_scene_object.py pose un positionnement en pixels absolus pensé
pour le canevas de scène (position:absolute, left/top/width/height en
dur, coordonnées d'origine de l'objet) — réinjecté tel quel comme
simple miniature 40×40 dans la carte, l'image se plaçait à ses
coordonnées de scène d'origine au lieu de rester dans son cadre.
Écrase ces styles en !important côté miniature seulement.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 19:08:45 +02:00
williamandClaude Sonnet 5 b64541856f Éditeur de collision : cartes "pièces qui s'emboîtent", animées
Build and deploy / test-python (push) Successful in 6m27s
Build and deploy / test-js (push) Successful in 56s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Refonte visuelle de l'onglet "🧩 Collision" — l'ancien rendu (pastilles
plates + flèches) ne rendait pas la métaphore "pièces de puzzle qui
s'emboîtent" demandée. Chaque maillon (déclencheur/action/sous-action)
est maintenant une pièce chevronnée colorée par type, imbriquée dans
la suivante via clip-path + marge négative, avec une entrée animée en
cascade. Cartes objet et choix de l'assistant modal redessinés en
grille de cartes avec icône + libellé, hover/entrée animés.

Purement visuel — aucun changement de logique/données (déjà couvert
par tests/test_collision_rules.py et
static/js/play/__tests__/collision-rules-controller.test.js).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 17:23:42 +02:00
williamandClaude Sonnet 5 ee270d0286 Éditeur de collision (jeu 2D) : règles trigger -> action par objet
Build and deploy / test-python (push) Successful in 6m31s
Build and deploy / test-js (push) Successful in 50s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Nouvel onglet "🧩 Collision" dans l'éditeur de scène 2D : chaque objet
(hors joueur et fond) peut porter des règles "à la collision" ou "dans
un périmètre (px)" déclenchant une action (quête, attaque, événement,
ou interagir - qui affiche "Appuie sur [touche]" puis exécute une
sous-action à l'appui, un seul niveau d'imbrication). Backend
(sanitisation, route de persistance, exposition dans
full_game_payload) + assistant modal en cartes empilées côté client.

Ajoute le moteur d'exécution runtime (collision-rules-controller.js) :
détecte l'entrée en collision/périmètre avec le joueur à chaque tick,
déclenche l'action une seule fois par entrée, pur JS client sans appel
serveur (fonctionne à l'identique en ligne et en export SCORM).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 17:11:42 +02:00
williamandClaude Sonnet 5 dfaf85fac1 Retire la collision sur les images de fond + les blocs de logique/timeline de l'éditeur 2D
Build and deploy / test-python (push) Successful in 6m55s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Deux retours utilisateur sur l'éditeur de scène 2D :

- Le panneau "🧱 Collision" (réglages + aperçu visuel sur le canevas)
  n'apparaît plus pour un objet kind="fond" : jamais un obstacle ni une
  cible de collision possible (déjà exclu de forgeSolidObstacles côté
  jeu), ce panneau n'aurait donc aucun effet.

- Retire les onglets "🧩 Blocs de logique" et "🎬 Timeline d'animation"
  de CET éditeur (l'éditeur document, templates/screen_edit.html, les
  garde tel quel) — en prévision d'un éditeur dédié aux scènes 2D
  (collision/périmètre -> action), pas encore construit. switchBuilderTab()/
  toggleDashCreate() (génériques, nécessaires à l'onglet "Événements"
  restant) extraites dans un nouveau builder-tabs.js partagé par les deux
  éditeurs, pour ne plus avoir à charger flow-editor.js/tabs-and-blocks.js/
  animation-timeline.js (propres aux blocs/timeline) dans l'éditeur 2D.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 16:41:56 +02:00
williamandClaude Sonnet 5 a231bcf2fa Aperçu de la boîte de collision dans l'éditeur + blocage physique au jeu
Build and deploy / test-python (push) Successful in 6m15s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Deux retours utilisateur sur la boîte de collision (ajoutée précédemment) :

- Visible et réglable directement dans l'éditeur de scène : pour l'objet
  sélectionné, un contour pointillé (rectangle/cercle) se superpose sur
  le canevas — glisser son corps ajuste le décalage, glisser sa poignée
  (coin bas-droit) ajuste la taille, les deux se répercutent dans le
  panneau "🧱 Collision" et inversement (édition des champs -> aperçu à
  jour). static/js/scenes/scene-editor.js::onCollisionBoxMouseDown/
  onCollisionBoxResizeMouseDown, mirror des poignées de position/taille
  déjà existantes pour l'objet lui-même.

- Bloque désormais RÉELLEMENT le déplacement au clavier : avant chaque
  pas, personnage-controller.js teste si la position candidate
  chevaucherait la boîte de collision d'un autre objet solide (jamais un
  "fond", jamais un objet à collision désactivée) et annule ce pas — par
  AXE séparément, pour permettre de glisser le long d'un mur en
  diagonale plutôt qu'un blocage total au moindre contact. Réutilise
  forgeShapesOverlap (conditions.js, factorisé depuis elementsOverlap
  pour ne jamais dupliquer la règle "qu'est-ce qui se touche").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:48:08 +02:00
williamandClaude Sonnet 5 bc3feb5080 Panneau scène simplifié + boîte de collision + rôle joueur/ennemi/pnj
Build and deploy / test-python (push) Successful in 11m52s
Build and deploy / test-js (push) Successful in 56s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Trois retours utilisateur sur l'éditeur de scène 2D :

- Retire l'arborescence "Objets de cette scène" (un objet reste
  sélectionnable en cliquant dessus sur le canevas) et le bouton
  "+ Ajouter un décor" (le type "decor" reste supporté côté serveur,
  juste plus accessible depuis ce menu).

- Chaque objet de scène a désormais une boîte de collision AUTOMATIQUE
  (= sa propre boîte, comportement inchangé pour une condition de
  collision déjà posée) réglable dans un nouveau panneau "🧱 Collision" :
  activée/désactivée, forme (rectangle/cercle), taille et décalage —
  utile pour un sprite très paddé (CraftPix) dont la silhouette réelle
  est bien plus petite que son canevas. elementsOverlap() (conditions.js)
  applique ces réglages en restant identique par défaut.

- Nouveau champ "🏷️ Rôle" (Joueur/Ennemi/PNJ) sur un personnage : SEUL un
  personnage "Joueur" est désormais déplacé/animé au clavier et suivi
  par la caméra (personnage-controller.js) — "Ennemi"/"PNJ" restent
  immobiles tant qu'aucune logique de flow ne les pilote (déplacement
  ennemi automatique/apparition, dialogue de PNJ... hors scope de ce
  réglage, qui ne fait qu'identifier "quel objet est le joueur").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 15:30:34 +02:00
williamandClaude Sonnet 5 9199897784 Image de fond de scène + caméra qui suit le personnage, fps d'animation cohérent
Build and deploy / test-python (push) Successful in 6m26s
Build and deploy / test-js (push) Successful in 57s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Trois retours utilisateur :
- fps d'idle/interagir/touches supplémentaires alignés sur celui de la
  marche (8 i/s partout, était 4 pour idle — perçu comme "les autres
  animations sont lentes à côté de la marche").
- Nouveau menu "🏞️ Images de fond" dans "Objets de cette scène" : pose
  un objet kind="fond" à la taille RÉELLE de l'image choisie (pack
  CraftPix intégré en galerie, admin seulement — licence, voir
  core/sprite_gate.py), derrière tout le reste, insensible au clic.
- Si ce fond dépasse la scène, elle devient le "monde" : .playScreen.
  playScene est désormais le viewport (overflow:hidden, taille fixe),
  .sceneWorld le monde à l'intérieur — la caméra centre le premier
  personnage trouvé, bornée pour ne jamais montrer au-delà des bords
  (voir personnage-controller.js::forgeUpdateSceneCamera). Le personnage
  peut désormais se déplacer sur tout le monde, pas seulement le petit
  cadre visible (clampSceneObjectPosition, actions.js). Un jeu sans fond
  XXL garde un comportement strictement identique à avant (monde ==
  scène, transform vide).

Bibliothèque générée une fois par scripts/generate_background_manifest.py
(assets/background/, non versionné, licence CraftPix) vers
static/backgrounds/ (committé), même patron que
generate_animal_sprite_manifest.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 12:24:10 +02:00
williamandClaude Sonnet 5 5891c82c62 Commandes personnage : détection de touche au clavier + touches d'animation supplémentaires
Build and deploy / test-python (push) Successful in 6m17s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Deux retours utilisateur sur le panneau "Commandes" (déplacement/
animation automatiques, voir précédent commit) :
- Les champs de touche (haut/bas/gauche/droite/interagir) capturent
  maintenant la touche au clavier (clic puis appui — event.key, même
  valeur que heldKeys/triggers.js) au lieu d'être tapés à la main,
  source d'erreurs ("Espace" vs " ", "flèche haut" vs "ArrowUp"...).
- Nouvelle section "Animations supplémentaires" : un nombre illimité de
  touches, chacune liée à UNE pose au choix parmi celles réellement
  disponibles pour ce personnage (sauter, attaquer, courir...) — pas
  seulement les 4 touches de déplacement + interagir.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:50:29 +02:00
williamandClaude Sonnet 5 74f7ca396f Déplacement/animation automatiques d'un personnage (scène 2D)
Build and deploy / test-python (push) Successful in 6m25s
Build and deploy / test-js (push) Successful in 55s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Simplification demandée pour l'éditeur de jeu RPG : un personnage posé
sur une scène 2D se déplace et s'anime TOUT SEUL avec ZQSD (+ E pour
interagir) dès qu'il est posé — plus besoin de poser le moindre nœud de
flow pour un mouvement de base. Le panneau "🕹️ Commandes" du personnage
permet de remapper les 4 touches de déplacement et la touche
d'interaction, et de bloquer un axe (horizontal/vertical seulement).

Entièrement client (static/js/play/personnage-controller.js), réutilise
heldKeys (triggers.js) et applyObjectProperty/clampSceneObjectPosition/
runSpriteAnimation (actions.js) — aucune logique dupliquée, et ça
fonctionne aussi bien en ligne que dans l'export Web/SCORM (aucune
requête serveur). "walk"/"idle"/"interact" (poses déjà présentes pour
tout personnage Forge/CraftPix) sont utilisées telles quelles ; une pose
absente dégrade silencieusement (déplacement sans animation) plutôt que
de planter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 11:26:37 +02:00
williamandClaude Sonnet 5 82d00b3800 Export Web/SCORM : inline les icônes en data: URI (CORS Firefox/file://)
Build and deploy / test-python (push) Successful in 6m16s
Build and deploy / test-js (push) Successful in 54s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Le chemin relatif ("icons/x.svg") était enfin correct, mais Firefox
bloque encore par CORS TOUT chemin de fichier pour mask-image sous
file:// — chaque ressource file:// y est une origine opaque distincte,
indépendamment du chemin (troisième rapport du même utilisateur). Seule
une data: URI (aucune requête réseau séparée) contourne le problème.
_build_icon_data_uris (build_scorm_package.py) encode chaque icône
RÉELLEMENT posée dans le jeu (élément direct ou niché dans un élément de
jeu réutilisable) en base64, ajouté au payload sous
gameData.icon_data_uris ; forgeRenderIcone (JS) le consulte pour toute
régénération après une action, avec repli sur l'ancien chemin relatif
si jamais une icône n'a pas été pré-encodée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 10:27:04 +02:00
williamandClaude Sonnet 5 b60fe63a54 Export Web/SCORM : corrige le chemin de l'icône doublé "static/static/"
Build and deploy / test-python (push) Successful in 6m44s
Build and deploy / test-js (push) Successful in 56s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Le premier correctif ("static/icons/x.svg") ne suffisait pas : cette URL
est posée en style inline (--icon-url) mais CONSOMMÉE par `mask-image:
var(--icon-url)` dans static/style.css (.icon-svg) — une URL relative
dans une propriété personnalisée CSS se résout par rapport à la feuille
de style où le var() est UTILISÉ, pas où elle est définie (piège CSS
connu). Résultat : "static/icons/x.svg" redevenait
"static/static/icons/x.svg" une fois résolu depuis static/style.css
(CORS bloqué — deuxième rapport du même utilisateur). Seul "icons/x.svg"
(sans le préfixe "static/") est correct une fois résolu depuis
static/style.css. Corrigé côté rendu initial
(_relativize_absolute_urls) et côté port JS (forgeRenderIcone, qui
régénère ce widget après une action).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 10:12:44 +02:00
williamandClaude Sonnet 5 e08c53e042 Export Web/SCORM : corrige la résolution des champs "relation" hors ligne
Build and deploy / test-python (push) Successful in 6m55s
Build and deploy / test-js (push) Successful in 1m4s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Bug signalé : une Donnée liée/un filtre référençant un champ "relation"
affichait "{{champ}}" tel quel une fois exporté, alors qu'il fonctionnait
en ligne. Deux causes cumulées :

- full_game_payload.py lisait la colonne SQL "<champ>" au lieu de
  "<champ>_id" (seule vraie colonne d'un champ relation, voir
  _field_column dans filter_repeater_rows.py) en construisant
  gameData.data — une valeur toujours None. Invisible en ligne (les
  filtres y requêtent la base fraîche, jamais cette snapshot), mais
  fatal hors ligne (aucune base à requêter).
- Une fois ce None corrigé, le port JS (forgeFieldColumn) relisait
  cette même valeur sous une clé slugifiée+suffixée ("boss_id") alors
  que gameData.data est déjà indexé par le nom D'AFFICHAGE du champ
  ("boss") — mismatch qui ne se voyait que sur un champ relation (le
  seul cas où slugify(nom)+suffixe diverge du nom original). Supprime
  ce port erroné, lit directement row[fieldName] partout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 10:02:26 +02:00
williamandClaude Sonnet 5 7bc4c89eaa Export Web/SCORM : corrige les URLs absolues cassées en local (file://)
Build and deploy / test-python (push) Successful in 13m15s
Build and deploy / test-js (push) Successful in 1m4s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Bug signalé : icône disparue et texte suivant du dialogue absent après
export. render_icone.py (et son miroir JS, forgeRenderIcone) bakaient
une URL "/static/icons/..." en dur, correcte en ligne (racine du
serveur Flask) mais bloquée par CORS une fois ouverte en local
(file:///static/...). Même souci pour les fichiers envoyés par le
créateur (/game/<slug>/uploads/..., jamais dans static/, jamais copiés
par le paquet). Ajoute _relativize_absolute_urls (réécriture globale
post-rendu du HTML) + copie du dossier uploads/, et relativise le port
JS de l'icône pour toute régénération après action en mémoire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 09:31:44 +02:00
williamandClaude Sonnet 5 f3b72d73c9 Export Web/SCORM : port complet du runtime jouable côté navigateur
Build and deploy / test-python (push) Successful in 9m20s
Build and deploy / test-js (push) Successful in 1m6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 1.2 de la feuille de route produit — un paquet SCORM tourne seul
dans le LMS du client, sans serveur Forge Engine disponible. Ajoute
static/js/play/offline/ (miroir JS de screens/rendering/ et
data_actions/, sous window.FORGE_OFFLINE) pour que variables, score,
données d'objet, répéteurs et conditions de visibilité fonctionnent
entièrement en mémoire côté navigateur ; branche actions.js et
bindings.js dessus au lieu d'un fetch() serveur. Ajoute
publish/build_scorm_package.py (paquet statique + imsmanifest.xml
SCORM 1.2 + wrapper API SCORM) et le bouton "Exporter (Web/SCORM)".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 07:43:02 +02:00
williamandClaude Sonnet 5 9776fbc057 Score/Progression (Phase 1.1 de la feuille de route produit)
Build and deploy / test-python (push) Successful in 7m27s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Concept de premier ordre, distinct du système de variables globales —
préalable identifié aux futurs exports SCORM/xAPI (note de cadrage
.claude/Forge_Engine_Cadrage.pdf) : ces standards ont besoin d'un signal
"score"/"terminé" propre, pas d'une convention sur une variable choisie
par le créateur.

- db/scoring/ (même patron que db/global_vars/) : table _scoring, un
  score numérique + un statut (non_commence/en_cours/termine/reussi/
  echoue) par joueur (voir db.PLAYER_SHARED pour l'aperçu créateur).
- Deux nouvelles actions de flux, dans les deux éditeurs (document et
  scène) : "Modifier le score" (réutilise le vocabulaire d'opérations
  déjà là pour "Modifier une variable" — incrémenter/définir/etc., aucune
  nouvelle colonne de nœud) et "Définir le statut de la partie".
- Routes créateur (routes/flow/flow_node_run_score.py,
  flow_node_run_status.py) + miroirs publics par joueur
  (routes/public_play/) — même principe que flow_node_run_variable.py.
- GET /game/<slug>/scoring/<player_id> : lecture interne, pas exposée au
  joueur, préparée pour être consommée par le futur export Web/SCORM.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:23:37 +02:00
williamandClaude Sonnet 5 76a50fa87a Onboarding guidé systématique + tableau de bord simplifié
Build and deploy / test-python (push) Failing after 1m48s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Comportement SYSTÉMATIQUE à chaque création de jeu (admin compris), pas
une formalité réservée à l'inscription : /onboarding (routes/onboarding/)
devient le point d'entrée unique — 4 cartes retournables (survol =
explication au dos), défilement horizontal animé vers le nom du jeu. Un
admin y repasse à volonté (pas de project_slug dédié, jamais bloqué/
redirigé vers un projet précédent) ; un compte "user" n'en a plus qu'un
créé d'office (routes/auth/register_2fa.py), guidé ici à la place.

- db/games/game_type_catalog.py : catalogue des 4 types (Quiz/
  Embranchement-escape game/RPG/Créer mon jeu de A à Z), _meta['onboarding_type']
  décide du "kind" du premier écran créé et si le tableau de bord complet
  reste accessible.
- routes/games/game_dashboard.py, templates/game_dashboard_simple.html :
  un type restreint (quiz/embranchement/rpg) voit désormais SON tableau
  de bord (même route que "custom"), rendu en version simplifiée — juste
  ses écrans en cartes avec un aperçu RÉEL du contenu (scène mise à
  l'échelle par container query CSS, adaptée à la largeur réelle de la
  carte). "+ Ajouter un écran" n'y propose pas de choix de type : imposé
  par le projet (routes/screens/screens_new.py), verrouillé aussi côté
  serveur.
- core/auth_guard.py : plus de blocage de game_dashboard par type — la
  restriction se fait au rendu, pas à l'accès à la route.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:22:45 +02:00
williamandClaude Sonnet 5 d5a59bdbd8 Fusion des deux moteurs : type d'écran par écran, plus par projet
Build and deploy / test-python (push) Failing after 6m10s
Build and deploy / test-js (push) Successful in 58s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
"document" (écrans %) et "jeu_2d" (scène pixels) devient une propriété
PAR ÉCRAN (_screens.kind, migration automatique idempotente dans
ensure_schema.py, source = l'ancien game_type au niveau projet) plutôt
qu'un choix figé pour tout le jeu — un même projet peut désormais
mélanger écrans classiques et scènes 2D librement.

- routes/screens/screen_edit.py : dispatch vers l'éditeur de scène selon
  screen["kind"] (l'écran demandé), plus game["game_type"].
- screens/payload/full_game_payload.py, templates/play.html,
  static/js/play/screens.js : le rendu jouable (payload, markup, bascule
  du mode plein-écran #playFrame) décide écran par écran, y compris en
  cours de partie (changer d'écran ne recharge pas la page).
- screens/screens_repo/create_screen.py : nouveau paramètre kind.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 06:21:13 +02:00
williamandClaude Sonnet 5 f767ed4fab Bibliothèque de sprites animaux CraftPix (3/3) : sprites générés
Build and deploy / test-python (push) Successful in 7m18s
Build and deploy / test-js (push) Successful in 1m2s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Dernier lot de la fonctionnalité (voir 449c36fd et a27d08c6) : les
sprites eux-mêmes, générés une fois par scripts/generate_animal_sprite_manifest.py
depuis les packs sources CraftPix (assets/characters/, non committés —
licence perso, voir .gitignore — gardés en local pour régénérer si besoin).

- static/characters/animals/manifest.json : catalogue déclaratif (14
  familles, 15 variantes de couleur chacune, poses/animations
  disponibles par famille) lu par screens/labels/animal_sprite_library.py
  au démarrage pour construire ADMIN_SPRITE_LIBRARY.
- static/characters/animals/<famille>/<variante>/ : les frames PNG de
  chaque pose, réencodées à une taille raisonnable pour le web (les
  canevas sources CraftPix sont surdimensionnés, ~788×504 px).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 19:25:55 +02:00
williamandClaude Sonnet 5 727af55a5e Mouvement continu, limites de scène et priorité d'animation (jeu 2D)
Build and deploy / test-python (push) Failing after 1m46s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Répond au manque signalé par l'utilisateur : le déclencheur "clavier"
existant (keydown) ne se déclenche qu'UNE FOIS par appui, insuffisant
pour "maintenir une touche fait avancer/sauter le personnage en continu".

- Deux nouveaux déclencheurs (screens/flow/constants.py,
  screens/scenes/flow_palette.py) : "Tant qu'une touche est maintenue"
  (se répète ~20 fois/seconde tant que la touche reste enfoncée,
  runScreenHeldKeyTriggers() dans triggers.js — même patron PAR ÉCRAN que
  "minuteur", arrêté au changement d'écran) et "Au relâchement d'une
  touche" (un seul déclenchement, scan global comme "clavier"). Combinés
  à l'action existante "Modifier un objet de scène → Déplacer de... px
  (relatif)", ça permet un vrai déplacement continu.
- preventDefault() sur toute touche que le jeu écoute réellement
  (isGameKey(), triggers.js) : Espace/Flèches font défiler la page par
  défaut, et Espace réactive en plus le bouton actuellement focus (souvent
  le bouton "Jouer" qui garde le focus après l'ouverture de l'aperçu) —
  ça pouvait donner l'impression qu'une touche du jeu ne faisait rien.
- Le personnage pouvait sortir du cadre de la scène en se déplaçant :
  applyObjectProperty()/clampSceneObjectPosition() (static/js/play/actions.js)
  bornent maintenant toute position (absolue ou relative) à
  [0, scene_width/height − la taille de l'objet].
- Vitesse d'animation par défaut adaptée au nombre d'images : la valeur
  fixe (8 i/s) venait d'un formulaire pensé pour les cycles Kenney (8
  images) — un cycle CraftPix (walk=30 images) au même 8 i/s prenait
  ~4 secondes, "très lent". Le choix d'une animation dans la galerie
  calcule maintenant une vitesse par défaut proportionnelle à son nombre
  d'images (flow-editor.js, animation-timeline.js).
- Priorité d'animation (bug : "je ne peux pas me déplacer et sauter") :
  un déclencheur de déplacement (touche maintenue) redemande "marche" à
  chaque tick, écrasant aussitôt une animation ponctuelle ("sauter")
  démarrée entre-temps avant qu'elle ait pu s'afficher. runSpriteAnimation()
  (actions.js) laisse maintenant une animation NON BOUCLÉE en cours
  (même à une seule frame, ex. une pose Kenney figée) aller jusqu'au bout
  avant qu'une autre demande puisse l'interrompre.

Nouveaux tests : static/js/play/__tests__/{actions,triggers}.test.js
(idempotence + priorité d'animation, bornage aux limites de la scène,
isGameKey) ; tests/test_scene_edit_view.py (persistance d'un nœud
"touche_maintenue").

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:53:49 +02:00
williamandClaude Sonnet 5 c240181518 Fix éditeur de scène 2D : personnage déformé, animations muettes, galerie petite
Build and deploy / test-python (push) Failing after 1m52s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Trois correctifs remontés en test manuel sur l'éditeur de scène :

1. Personnage écrasé/déformé dans sa boîte de sélection : les frames
   CraftPix (screens/labels/animal_sprite_library.py) sont de grands
   canevas très paddés (~788×504 px), très différents des sprites Kenney
   déjà recadrés serré (~96×128 px) — sans object-fit, l'image remplissait
   quand même sa boîte (width/height:100%, voir la règle
   .canvasElementInner > img de scene_edit.html) mais DÉFORMÉE. Nouvelle
   classe .sceneObjectSprite (object-fit:contain) posée sur le <img> rendu
   par render_scene_object.py, éditeur ET jeu jouable.

2. Aucun clip de Timeline (sprite, Animate.css ou personnalisé) ne se
   jouait jamais sur un objet de scène : applyAnimationClip()
   (static/js/play/screens.js) et animation-timeline.js sélectionnent
   TOUJOURS leur cible via l'attribut data-anim-target (posé sur
   .canvasElement/.playElement pour le DOM), jamais data-element-id/
   data-object-id — cet attribut n'était simplement jamais posé sur les
   objets de scène. Ajouté sur le <img> rendu (render_scene_object.py).

3. Boîte par défaut d'un nouvel objet de scène trop petite (64×64 px sur
   une scène de 960×540) pour bien voir le personnage ou saisir la
   poignée de redimensionnement confortablement — portée à 128×128
   (ensure_scene_schema.py ; n'affecte que les NOUVEAUX objets, les
   objets déjà posés gardent leur taille actuelle).

Au passage : galerie de personnages ~3x plus grande (2 colonnes au lieu
de 6 dans .iconGallery.personnageGallery) — les tuiles étaient trop
petites pour bien distinguer les personnages.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:52:46 +02:00
williamandClaude Sonnet 5 f0070faced Phase A (2/2) : éditeur de scène 2D — route, template, runtime jouable
Build and deploy / test-python (push) Successful in 1m56s
Build and deploy / test-js (push) Successful in 8s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Deuxième et dernier commit de la Phase A du plan "Fondations d'une
plateforme multi-éditeurs" (le premier, efa7d1d, posait le schéma et le
CRUD des objets de scène). Ce commit branche l'éditeur et le mode
jouable sur ces fondations, en réutilisant TEL QUEL tout ce qui est déjà
générique côté moteur de logique.

Éditeur (routes/scenes/, templates/scene_edit.html,
static/js/scenes/scene-editor.js) :
- routes/screens/screen_edit.py délègue à render_scene_edit() dès que
  game_type == "jeu_2d" — même endpoint Flask "screen_edit" pour les
  deux éditeurs, aucune route dupliquée.
- Nouveau template scene_edit.html : canevas à taille FIXE en pixels
  (scene_width/scene_height) avec glisser-déposer/redimensionnement en
  pixels (scene-editor.js, mirror pixel de tree-panels.js), galerie
  personnages/décors. Les onglets "Blocs de logique"/"Timeline
  d'animation"/"Événements" réutilisent tels quels flow-editor.js,
  tabs-and-blocks.js et animation-timeline.js.
- flow-editor.js : nouveau flag FLOW_TARGETS_OBJECTS (false par défaut,
  true dans scene_edit.html) qui fait écrire submitNodeForm() vers
  trigger_object_id/target_object_id au lieu de trigger_element_id/
  target_element_id, plus une branche "Modifier un objet de scène"
  (position px, visibilité, orientation) et des gardes null partout
  (le DOM de scene_edit.html n'a pas tous les champs de l'éditeur
  document).
- blocks_view.py étend l'ensemble d'ids d'éléments concernés par un
  bloc pour inclure aussi trigger_object_id/target_object_id.

Runtime jouable (templates/play.html, static/js/play/actions.js,
screens/payload/full_game_payload.py) :
- full_game_payload() construit les écrans d'un jeu jeu_2d à partir de
  _scene_objects (render_scene_object) au lieu de _screen_elements, et
  récupère les animations de personnage sur les objets de scène.
- play.html : #playFrame occupe tout le viewport pour une scène jeu_2d
  (pas de ratio fluide) ; la scène se centre via .playScreen.playScene,
  qui réutilise tel quel showScreen() (déjà indexé par id d'écran, pas
  par forme DOM) — aucun fichier "scenes.js" séparé n'a été nécessaire.
- actions.js : jouer_animation_sprite retombe sur target_object_id si
  target_element_id est absent ; nouvelle action modifier_objet_scene
  avec applyObjectProperty() (positions en px, contrairement aux % de
  applyElementProperty()).

Un bug de gabarit a été découvert et corrigé pendant la vérification :
le commentaire CSS de play.html contenait littéralement "{% for %}"
comme texte français, que Jinja interprétait comme une vraie balise et
faisait planter le rendu — reformulé. render_scene_object() n'était en
outre jamais appelé par la vue de l'éditeur (les objets de scène
s'affichaient sans image) — scene_edit_view.py attache maintenant
rendered_html à chaque objet avant de les passer au template.

Nouveaux tests (tests/test_scene_edit_view.py, 10 cas) : dispatch
document vs jeu_2d, rendu d'un personnage sélectionné, CRUD géométrie/
suppression d'objet, nœuds de flow ciblant un objet de scène (action et
condition de collision), payload et page /play pour un jeu jeu_2d,
présence des nouveaux helpers JS. Suite complète : 309 tests passent
(299 existants + 10 nouveaux, aucune régression). node --check et
node --test (13 tests JS) passent également.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 10:05:40 +02:00
williamandClaude Sonnet 5 70c2b8df05 Corrige : une animation déjà posée dans la logique ignorait le changement de personnage
Build and deploy / test-python (push) Successful in 1m39s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Bug réel identifié grâce à la vidéo fournie + inspection directe de la
base de jeu de test : quand un nœud de flow "Jouer une animation"
(ou un clip de Timeline "sprite") était configuré pour un personnage,
ses frames étaient résolues et FIGÉES dans data_value/custom_keyframes
au moment de la configuration — changer ensuite le personnage Forge de
l'élément (galerie des propriétés) n'avait donc aucun effet sur les
animations déjà posées, qui continuaient à jouer indéfiniment les
frames de l'ANCIEN personnage.

Le nœud/clip ne stocke désormais que le NOM de l'animation
({"animation": "walk", "fps": 8, "loop": true}) — ses frames sont
résolues à l'EXÉCUTION, à partir du personnage ACTUELLEMENT assigné à
l'élément cible :
- screens/payload/full_game_payload.py expose un nouveau
  gameData.personnage_animations (élément → animations), reconstruit à
  chaque chargement de la page de jeu depuis _personnage_data — donc
  toujours à jour, y compris après un changement de personnage.
- static/js/play/actions.js (resolveSpriteFrames) et
  static/js/play/screens.js (applyAnimationClip) résolvent le nom
  d'animation en frames à ce moment précis, plutôt que d'utiliser des
  frames figées — repli sur l'ancien format {frames,...} pour les
  nœuds/clips déjà créés avant ce correctif.
- Éditeur (flow-editor.js/animation-timeline.js) : simplifié en
  conséquence — plus besoin de deviner rétroactivement quelle animation
  correspond à une liste de frames stockées (l'ancien hack de
  comparaison), le nom est maintenant stocké directement.

Nouveau test de régression (test_swapping_forge_character_updates_
already_configured_flow_action) qui reproduit exactement le scénario
filmé : configure l'action pour "male-adventurer", change le personnage
en "zombie", vérifie que gameData.personnage_animations reflète bien
zombie sans avoir à retoucher le nœud de flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 08:56:20 +02:00
williamandClaude Sonnet 5 5ead091c75 Personnages : toutes les animations, tous les personnages ; retrait de l'import de sprites personnalisés
Build and deploy / test-python (push) Successful in 1m50s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
À la demande de l'utilisateur (validation de la refonte visuelle du
widget Personnage), deux ajustements :

- Bibliothèque de sprites Forge étendue aux 6 personnages du pack Kenney
  "Toon Characters" (aventurier/aventurière, personnage homme/femme,
  robot, zombie), avec TOUTES leurs poses (45 par personnage) plutôt que
  3 — regroupées en 31 animations nommées par personnage (poses
  numérotées type walk0..walk7 fusionnées en un seul cycle "walk", les
  autres restant des poses figées à une image). ~1,2 Mo au total,
  toujours un sous-ensemble curé du pack source (assets/characters/, non
  versionné) — HD/Parts/Tilesheet/Vector toujours exclus.
- Import de sprites personnalisés retiré pour le moment : plus de tuile
  "➕ Sprite personnalisé" dans la galerie d'ajout, plus de section
  d'import dans les propriétés d'un personnage — seuls les personnages
  Forge restent proposés. La résolution serveur de _personnage_data
  garde son support générique de la source "custom" (aucune migration
  requise si cette possibilité revient plus tard), mais plus aucune UI
  ne permet de la créer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 07:38:22 +02:00
williamandClaude Sonnet 5 9e076f8cf9 Refonte : vrai widget "Personnage" visuel (remplace l'UI de la Phase 7)
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
L'utilisateur a testé la Phase 7 (sprites pilotés via un menu déroulant
+ zone de texte dans la logique de flow) et l'a rejetée à raison : ce
n'est pas comme ça qu'un moteur de jeu (Phaser, Unity) gère un
personnage. Cette refonte remplace tout le flux d'AUTEURING par un vrai
widget visuel — le moteur d'exécution de la Phase 7 (runSpriteAnimation,
activeSpriteAnimations, orientation, kind="sprite" de la Timeline) reste
inchangé.

Nouveau widget "personnage" (screens/widgets/registry.py) :
- Rendu serveur dédié (special_render, screens/rendering/render_personnage.py)
  qui affiche la pose "idle" dès le HTML généré — jamais une image
  cassée à configurer après coup.
- Toutes ses données (source Forge ou sprites propres au créateur, quel
  personnage/quelles animations) vivent dans une seule clé JSON
  _personnage_data (screens/rendering/personnage_data.py), sans aucune
  migration de schéma (même patron que c_clause_list).
- Nouvelle échappatoire "custom_panel", symétrique à "special_render" mais
  pour le panneau de propriétés : ce widget affiche une galerie/un import
  de sprites sur-mesure plutôt que les contrôles génériques.

Galerie visuelle de personnages Forge (VRAIES miniatures, pas un emoji) :
- Panneau gauche "🎭 Personnages" : pose un personnage déjà configuré,
  animé immédiatement.
- Panneau droit (propriétés) : change le personnage Forge de l'élément
  sélectionné, ou importe les animations d'un sprite personnalisé.
- static/js/screen_edit/personnage-preview.js : lance l'aperçu animé de
  CHAQUE personnage du canevas dès le chargement de la page et après
  toute sauvegarde — le cœur de la demande ("je dois voir ça bouger").

Sélecteur d'animation à miniatures (nœud de flow "Jouer une animation" et
clip de Timeline "sprite") : remplace le menu déroulant/la zone de texte
par une grille de vraies miniatures, scopée aux SEULES animations du
personnage réellement ciblé (ELEMENT_ANIMATIONS_MAP, résolu côté serveur
à partir des propriétés de cet élément précis) — jamais un catalogue
global. Le format stocké sur le nœud/le clip ({frames, fps, loop}) est
inchangé, seule l'UI d'édition change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 20:26:39 +02:00
williamandClaude Sonnet 5 d2c2f20b65 Phase 7 : personnage animé par sprites (poses/images successives)
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute une nouvelle action de flow "jouer_animation_sprite" qui joue une
séquence de PNG sur un élément image (cycle en boucle type marche, ou
une fois type saut) — réutilise target_element_id (déjà whitelisté) et
data_value en JSON {frames, fps, loop}, exactement le patron déjà établi
par "jouer_son" (Phase 6) et "alea" (Phase 2) : aucune nouvelle colonne,
aucune migration de schéma.

Ajoute une propriété d'élément "orientation" (Modifier un élément) pour
retourner un personnage en miroir (gauche/droite/bascule) sans nécessiter
une 2e feuille de sprites "vue de dos" — même patron que "surbrillance"/
"désactivé".

Étend aussi la Timeline d'animation existante (kind="sprite", aux côtés
de "animate_css"/"custom") pour qu'un personnage puisse animer tout seul
dès l'affichage de l'écran (ex. idle en boucle perpétuelle), pas
seulement en réaction à un événement — réutilise la colonne générique
custom_keyframes (JSON) et le réglage iteration_count déjà là pour la
boucle, aucune migration non plus. Les deux entrées (action de flow et
clip de timeline) partagent le même moteur côté client
(runSpriteAnimation()/activeSpriteAnimations dans static/js/play/
actions.js, indexé par nœud DOM plutôt que par id d'élément pour
supporter plusieurs instances d'un même écran-modèle animées
indépendamment).

Une bibliothèque de sprites Forge (2 personnages Kenney CC0, sous-
ensemble curé idle/marche/saut copié dans static/characters/) est
proposée dans l'éditeur, mais un créateur peut tout aussi bien utiliser
ses propres sprites uploadés (même route d'upload générique que le son
de la Phase 6).

Périmètre volontairement limité à ce qui a été demandé : pas d'avatar
modulable en couches (assemblage cheveux/haut/bas par le joueur),
écarté du plan initial à la demande de l'utilisateur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 19:18:07 +02:00
williamandClaude Sonnet 5 76331f9285 Phase 6 : son (musique de fond par écran + action "Jouer un son")
Build and deploy / test-python (push) Successful in 1m27s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute deux mécanismes distincts, à la demande de l'utilisateur qui a
précisé qu'un son doit pouvoir être attaché à une SCÈNE (pas seulement
joué ponctuellement par une action de flow, comme prévu initialement) :

- Musique de fond par écran (nouveau champ background_music_url sur
  _screens, ALTER TABLE nullable) : réglée dans le panneau gauche de
  l'éditeur (URL ou envoi de fichier, réutilise la route d'upload
  générique existante), enregistrée en AJAX au même patron que le format
  d'aperçu (screen_set_aspect.py). Démarrée en boucle à l'affichage de
  l'écran et arrêtée au changement d'écran (runScreenBackgroundMusic(),
  appelée depuis showScreen() dans static/js/play/screens.js) — un seul
  Audio actif à la fois, jamais cumulé avec une musique restée d'un écran
  précédent.

- Action de flow "jouer_son" (aux côtés des actions existantes) : effet
  sonore ponctuel, non bouclé, déclenchable sur n'importe quel nœud
  Déclencheur. Réutilise data_value (déjà un champ texte générique sur
  le nœud action, comme pour "attendre") plutôt qu'une nouvelle colonne
  dédiée — fire-and-forget côté client (runActionNode), ne bloque jamais
  la suite du graphe.

Les deux réutilisent le même mécanisme d'upload de fichier déjà en place
ailleurs dans l'éditeur (ex. source d'une vidéo), sans nouvelle route.

Ceci complète les 6 phases du plan d'extension du moteur (état par
joueur, hasard/maths, clavier/minuteur, ajout de ligne, position/
collision, son) : Forge Engine peut désormais couvrir des jeux bien
au-delà du narratif/puzzle/quiz (action, arcade, jeux à contrainte de
temps).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:56:49 +02:00
williamandClaude Sonnet 5 9e263435e6 Phase 5 : position/déplacement d'élément + condition de collision
Build and deploy / test-python (push) Successful in 1m29s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Ajoute le positionnement absolu (pos_x/pos_y, réutilise left/top en %
déjà en place) et relatif (pos_x_relatif/pos_y_relatif, ajoute un delta
à la position actuelle plutôt que de l'écraser) comme nouvelles
propriétés de l'action "Modifier un élément".

Ajoute une nouvelle source de condition "collision" (aux côtés de
"objet"/"variable") : deux éléments (cond_element_a/cond_element_b,
ALTER TABLE sans contrainte FK, même patron que block_id/
trigger_custom_event_id) dont on compare les rectangles à l'écran via
getBoundingClientRect() côté client (elementsOverlap(), dans
conditions.js). Pas d'opérateur/valeur à choisir : le chevauchement EST
directement le booléen vrai/faux du nœud — le créateur relie le port
"Faux" pour "ne se touchent pas", exactement comme pour n'importe quelle
autre condition (design plus simple que réinterpréter égal/différent,
qui n'a pas de sens pour superieur/inferieur).

delete_element.py et flow_nodes_referencing_element.py nettoient
désormais aussi les nœuds de collision référençant un élément supprimé
(ou l'un de ses descendants), pour rester cohérents avec le nettoyage
déjà en place pour trigger_element_id/target_element_id.

Combiné à la Phase 3 (minuteur récurrent) et au déplacement au clavier,
ça couvre des jeux type casse-briques/Pong/ramasse-objets sans
construire un vrai moteur physique (pas de vélocité/accélération/
gravité continues, cadrage volontairement limité).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:34:51 +02:00
williamandClaude Sonnet 5 92fbfbc9dd Phase 4 : ajout dynamique d'une ligne à l'exécution
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Nouveau sentinel LAST_INSERTED_ROW_ID = -3 (screens/flow/constants.py),
aux côtés de CLICKED_ROW_ID = -1 — même principe : un id de ligne
n'existe qu'APRÈS l'insertion, jamais connu à la création du nœud.
Permet d'enchaîner un nœud "Ajouter une ligne" (crée une ligne VIDE) puis
un ou plusieurs nœuds "Modifier une donnée" déjà existants (ciblant
"➕ Dernière ligne ajoutée" dans le sélecteur "Ligne concernée", partagé
avec les conditions) pour renseigner ses champs — réutilise 100% du
mécanisme actuel, aucun nouveau format de payload multi-champs.

screens/data_actions/apply_add_row_action.py : db.get_definition +
db.insert_row(slug, definition, {}, player_id) — la ligne appartient au
joueur qui agit pour un objet per_player (Phase 1).

Nouvelles routes (créateur ET publique, comme prévu dès la Phase 1) :
POST /game/<slug>/flow/nodes/<id>/run-add-row et son miroir
/jouer/<slug>/.../run-add-row — renvoient {"ok", "row_id"}.
routes/flow/flow_node_run_data.py (+ son miroir public) : résout aussi
LAST_INSERTED_ROW_ID (en plus de CLICKED_ROW_ID déjà en place) via
last_inserted_row_id transmis par le client.

static/js/play/actions.js : runActionNode branche "ajouter_ligne" ->
fetch la nouvelle route, pose window.lastInsertedRowId, puis
refreshRuntimeData() (un Répéteur lié affiche la nouvelle ligne au
prochain rendu, confirmé par l'audit préalable — aucun ajustement du
mécanisme de rafraîchissement nécessaire). La branche "modifier_donnee"
transmet désormais aussi last_inserted_row_id, comme clicked_row_id.

templates/screen_edit.html + static/js/screen_edit/flow-editor.js :
nouveau type d'action "Ajouter une ligne à un objet" (juste un
sélecteur d'objet, aucun champ à remplir — le rappel du fonctionnement
enchaîné est affiché directement dans le formulaire) ; le sélecteur
"Ligne concernée" (partagé Condition/Modifier une donnée) gagne l'option
"➕ Dernière ligne ajoutée" à côté de "🖱️ Ligne cliquée".

Vérifié : 259 tests passent (4 nouveaux, dont un bout-en-bout via HTTP
qui enchaîne réellement les deux nœuds et vérifie le champ renseigné, et
un qui verrouille l'isolation par joueur de la ligne créée), 13 tests
node:test toujours au vert, syntaxe JS validée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:18:10 +02:00
williamandClaude Sonnet 5 727c3c97ba Phase 3 : déclencheur clavier + minuteur récurrent
Build and deploy / test-python (push) Successful in 1m28s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
screens/flow/constants.py : TRIGGER_EVENTS += "clavier" (À l'appui sur
une touche) et "minuteur" (Toutes les X millisecondes) — ni élément ni
écran précis pour les deux, comme "evenement" déjà en place.
FLOW_NODE_FIELDS += trigger_key/trigger_interval_ms. ensure_flow_schema.py :
ALTER TABLE pour les 2 colonnes (patron trigger_custom_event_id).

templates/screen_edit.html + static/js/screen_edit/flow-editor.js :
- "clavier" : un champ "Touche à surveiller" qui capture lui-même la
  touche pressée (onkeydown sur l'input, captureFlowTriggerKey()) plutôt
  que de faire deviner la syntaxe attendue (ev.key du navigateur, ex.
  "ArrowUp", "a", " " pour Espace).
- "minuteur" : un simple champ numérique (millisecondes).
- nodeLabel() affiche "⌨️ Touche « X »"/"⏱️ Toutes les N ms" sur le nœud.

static/js/play/triggers.js (moteur de jeu) :
- bindKeyboardTriggers() : UN SEUL window.addEventListener('keydown', ...)
  posé une fois pour tout le jeu (voir l'amorçage en fin de
  templates/play.html) — même patron de scan global que
  dispatchGameEvent() pour "Sur un événement personnalisé".
- runScreenTimerTriggers(screenId) : géré PAR ÉCRAN (appelé depuis
  showScreen(), static/js/play/screens.js) — démarre les setInterval des
  nœuds "minuteur" de l'écran affiché, arrête d'abord tous ceux de
  l'affichage précédent (même principe que runAnimationTimeline) pour
  ne jamais accumuler des minuteurs sur des écrans quittés.

Vérifié : 255 tests passent (5 nouveaux, dont un qui verrouille que la
touche Espace — très probablement utilisée en jeu — n'est pas filtrée
comme une valeur "vide" par add_flow_node.py), 13 tests node:test
toujours au vert, syntaxe JS validée sur tous les fichiers de
static/js/play/ et static/js/screen_edit/. Comme le reste du graphe de
logique côté client, le comportement RÉEL d'un keydown/setInterval n'est
pas testable sans navigateur — test manuel recommandé (touche assignée
à un saut, minuteur faisant avancer un compteur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:05:55 +02:00
williamandClaude Sonnet 5 7476ed229e Phase 2 : hasard + opérations mathématiques
Build and deploy / test-python (push) Successful in 1m26s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
screens/labels/data_operations.py : 6 nouvelles opérations pour
"Modifier une donnée"/"Modifier une variable" — multiplier, diviser,
modulo (garde-fou division par zéro : valeur inchangée plutôt qu'une
ZeroDivisionError qui interromprait le graphe), minimum/maximum (borne
la valeur ACTUELLE — utile pour une variable globale, qui n'a pas de
min_value/max_value comme un champ d'objet), et alea (tire un nombre
aléatoire entre deux bornes).

screens/data_actions/compute_operation.py (déjà factorisé en Phase 0,
donc une seule implémentation pour apply_data_action.py/
apply_variable_action.py) : implémente les 6. "alea" est la seule à
deux opérandes — réutilise data_value au format "min,max" plutôt qu'une
nouvelle colonne de nœud (bornes remises dans l'ordre si inversées).
random.uniform pour un résultat décimal, random.randint pour un entier.

static/js/screen_edit/flow-editor.js + templates/screen_edit.html :
petit indice visuel — le champ "Valeur / montant" du formulaire de nœud
affiche "min,max (ex. 1,6)" quand "alea" est choisi, pour ne pas laisser
deviner ce format à deux nombres, différent de toutes les autres
opérations. Sinon aucun nouveau champ/changement de schéma nécessaire,
le <select> était déjà généré depuis DATA_OPERATIONS.

Vérifié : 250 tests passent (19 dans test_compute_operation.py, dont un
qui a dû être corrigé — il utilisait "multiplier" comme exemple
d'opération INCONNUE, devenu un mauvais exemple maintenant qu'elle
existe), 13 tests node:test toujours au vert, syntaxe JS validée.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 16:47:59 +02:00
williamandClaude Sonnet 5 fe807ba51e Phase -1 (suite) : découpe l'éditeur de screen_edit.html en modules JS
Même chantier que le commit précédent (moteur de jeu, play.html) —
templates/screen_edit.html était un unique fichier HTML+CSS+JS de 3312
lignes, tout l'éditeur (arborescence, panneaux flottants, canevas,
formulaire de propriétés, éditeur de flow à nœuds, blocs de logique,
timeline d'animation) vivant dans UN SEUL <script>.

Ce fichier est plus imbriqué que play.html : de nombreux appels
s'exécutent au niveau racine du script (pas seulement des déclarations
de fonctions), et JavaScript hoiste les déclarations `function` sur
TOUT le script — un appel au niveau racine peut donc référencer une
fonction déclarée PLUS LOIN dans le même fichier. Découper naïvement
casserait cet ordre implicite. Un audit dédié (analyse ligne par ligne
de chaque appel racine + son graphe d'appel transitif) a identifié 3
références "en avance" réelles, toutes regroupées dans la même zone
(initBuilderPanel()/toggleActionFields() → bindAspectButtons/
toggleElementPropertyValue/onDataDefinitionChange) — le découpage
respecte cette contrainte : chaque fichier est une TRANCHE SÉQUENTIELLE
de l'original (jamais une réorganisation), et cette zone spécifique
reste un seul fichier (panel-init.js) pour que le hoisting continue de
fonctionner exactement comme avant.

5 fichiers sous static/js/screen_edit/ :
- tree-panels.js — arborescence, menu contextuel, panneaux flottants
  gauche/droite, galerie d'icônes, modale de suppression/choix d'icône,
  glisser-déposer du canevas, panneau de propriétés (autosave).
- panel-init.js — (ré)initialisation du panneau central après chaque
  changement de sélection, filtres de répéteur/donnée liée, condition
  de visibilité, champs d'action du formulaire de nœud.
- flow-editor.js — éditeur de flow à nœuds (rendu du graphe, formulaire
  d'ajout de nœud, blocs de logique — currentBlockNodes/Edges).
- tabs-and-blocks.js — onglets du centre, panneaux flottants génériques
  (drag/resize/plein écran), modale d'un bloc de logique.
- animation-timeline.js — timeline d'animation (clips Animate.css/
  personnalisés).

Toutes les données injectées par Jinja (GAME_SLUG, SCREEN_ID,
DEFINITIONS_DATA, FLOW_NODES_INITIAL, ELEMENTS_LABELS, CUSTOM_EVENTS_MAP,
ANIM_CLIPS...) sont posées UNE FOIS par un petit <script> inline resté
dans le template, avant les <script src> — même patron que
static/js/play/. Le seul bout de logique resté inline est la toute
petite IIFE d'ouverture initiale (?tab=/?block=), qui dépend directement
de request.args et doit s'exécuter après que tous les fichiers soient
chargés.

tests/conftest.py : screen_edit_js_bundle() (même principe que
play_js_bundle(), Phase -1 précédente) — 4 tests qui vérifiaient la
présence de telle fonction/chaîne dans le HTML de l'éditeur (le JS y
était inline) sont mis à jour pour chercher dans ce bundle. Un des deux
échecs révélait un test déjà fragile (assert "Ligne cliquée" in html
vérifiait en réalité le TEXTE SOURCE d'un <script> inline, jamais du
HTML réellement rendu — ce texte ne peut plus s'y trouver une fois la
fonction qui le construit dynamiquement déplacée dans un fichier
externe) : corrigé pour vérifier le bundle JS + la disponibilité de la
route séparément.

Vérifié : 215 tests passent, syntaxe JS validée sur les 5 nouveaux
fichiers (node --check) et sur les <script> inline restants (rendus via
le client de test). Test manuel recommandé (édition complète d'une
scène : arborescence, propriétés, glisser-déposer, logique de flow,
blocs, timeline) avant de considérer ce découpage définitivement sans
risque — comme pour play.html, ce fichier n'a pas de harnais de test
DOM automatisé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 14:38:57 +02:00
williamandClaude Sonnet 5 dbada333d5 Phase -1 : découpe le moteur de play.html en modules JS + premiers tests JS
templates/play.html était un unique fichier HTML+CSS+JS de 1069 lignes,
tout le moteur de jeu vivant dans UN SEUL <script>, sans aucune
couverture de test sur cette logique (seuls le rendu HTML et la syntaxe
JS étaient vérifiés). La feuille de route à venir (état par joueur,
hasard, clavier/minuteur, position/collision, son — voir le plan) va
justement faire grossir ce moteur : "un fichier = une fonction, un
dossier = une responsabilité" s'applique aussi au JS, pas seulement au
Python — le moment de découper est avant d'ajouter encore plus de code,
pas après.

Découpage en 6 fichiers sous static/js/play/, calqués sur les sections
déjà présentes dans le code (aucune réorganisation de logique, une pure
extraction) : screens.js (affichage d'écran, timeline d'animation),
conditions.js (évaluation des conditions — la partie 100% PURE, sans
DOM, la plus testable), actions.js (exécution des actions), triggers.js
(recherche des nœuds déclencheurs, attache des écouteurs), bindings.js
(résolution des {{champ}}, rafraîchissement des données), flow-engine.js
(parcours du graphe, événements personnalisés).

Zéro nouvel outillage : plusieurs <script src> dans l'ordre, partageant
le même espace global qu'avant (aucun bundler, aucune étape de build).
Les 2 URLs de route dont ces fichiers ont besoin (flow_node_run_data/
run_variable, runtime_payload) ne peuvent plus être injectées par Jinja
directement dans le code (un fichier statique n'est jamais passé par le
moteur de templates) — elles sont maintenant posées une fois dans
window.FORGE_PLAY_URLS par le petit <script> inline restant dans
play.html, qui ne porte plus que les données Jinja (gameData) et
l'amorçage (bindClicks() etc. au chargement).

publish/build_package.py : ajoute static/js/play à la liste des fichiers
copiés dans l'exécutable exporté (le mode jouable en dépend désormais).

Premiers tests JS (static/js/play/__tests__/conditions.test.js, lancés
via `node --test`, zéro nouvelle dépendance npm — decision prise avec
l'utilisateur de commencer par la logique PURE seulement, pas par une
couverture DOM via jsdom) : compareValues, resolveVariablePath,
evaluateConditionClause/Node, exactement la logique que les phases à
venir (opérations mathématiques, condition de collision) vont étendre.

tests/conftest.py : nouveau helper play_js_bundle() (concatène tout
static/js/play/*.js) — 13 tests existants qui vérifiaient la présence de
telle fonction/chaîne dans le HTML de /game/<slug>/play (tout le JS y
était inline avant ce découpage) sont mis à jour pour chercher dans ce
bundle à la place ; les tests qui vérifient un CSS/HTML réellement resté
dans play.html (forgeHighlight, forgeDisabled, #playFrame...) continuent
de chercher dans le HTML.

Vérifié : 215 tests pytest passent (aucune régression comportementale,
juste une réorganisation), 13 tests node:test passent, node --check sur
chacun des 6 nouveaux fichiers. Test manuel recommandé (jeu joué de bout
en bout : navigation, clic, survol, répéteur, condition, animation)
avant de considérer le découpage définitivement sans risque.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 13:59:11 +02:00
williamandClaude Sonnet 5 a445b72a6e Corrige "Ouvrir"/"Modifier" inertes dans l'onglet Blocs de logique
Deux bugs distincts, signalés par capture d'écran (bouton "Ouvrir" sans
effet, ligne d'édition affichée en permanence au lieu d'être cachée) :

1. onclick="openLogicBlockPanel({{ b.id }}, {{ b.name|tojson }})" cassait
   l'attribut HTML : tojson produit des guillemets DOUBLES (valides en
   JSON), qui terminaient prématurément l'attribut onclick="..." lui-même
   entre guillemets doubles — le gestionnaire de clic généré était donc
   tronqué et invalide, provoquant une erreur JS non interceptée qui
   arrêtait aussi tout le script restant dans la même balise <script>
   (dont l'IIFE qui devait poser window.openLogicBlockPanel). Corrigé en
   ne passant que l'id dans l'attribut et en retrouvant le nom du bloc
   côté client depuis FLOW_BLOCKS (déjà chargé) — plus aucune chaîne
   utilisateur à échapper dans un attribut HTML.

2. <tr class="hidden" id="blockEditRow..."> ne se cachait jamais : le
   CSS ne définissait .hidden que scopé (.floatPanel.hidden,
   .columnFilterMenu.hidden), jamais en règle générique — ajoutée dans
   styles/forge-custom.css.

Vérifié : 215 tests passent, syntaxe JS validée sur un scénario avec un
vrai bloc existant (reproduisant exactement la situation signalée),
onclick généré inspecté directement dans le HTML rendu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 11:48:25 +02:00
williamandClaude Sonnet 5 1b7706b357 Ajoute les blocs de logique : organise le graphe de flow en sous-graphes nommés
Le graphe de logique d'une scène s'affichait jusqu'ici sur un seul
canevas plat (toutes les scènes accumulant leurs nœuds sur la même
grille), ce qui ne tient pas à l'échelle dès qu'une scène évolue au fil
de l'avancée du joueur et accumule des centaines/milliers de nœuds.

Ajoute les "Blocs de logique" : un bloc regroupe un sous-ensemble de
nœuds/arêtes d'un écran sous un nom et une description (comme une
fonction). L'onglet "Logique de la scène" devient une liste de blocs
(nom, description tronquée à 3 phrases, éléments concernés, nombre de
nœuds, bouton "Ouvrir"). Ouvrir un bloc affiche SON graphe dans une
modale plein écran, redimensionnable et déplaçable (patron déjà mûr
dans game_dashboard.html, porté tel quel : makeFloatPanelDraggable/
Resizable/Fullscreenable).

Décision d'architecture : un bloc est un automate FERMÉ — impossible de
relier un nœud d'un bloc à un nœud d'un autre bloc (rejeté côté serveur
dans flow_edge_add.py). Toute communication entre deux blocs passe par
le système d'événements personnalisés déjà en place
(declencher_evenement / trigger_event="evenement").

Détails techniques :
- Nouvelle colonne _flow_nodes.block_id (nullable, sans FK — même
  rationale que trigger_element_id/target_element_id, voir
  screens/elements/delete_element.py) et nouvelle table _flow_blocks
  (screens/flow/ensure_flow_schema.py,
  screens/flow/blocks/ensure_flow_blocks_schema.py).
- Migration douce et automatique : les nœuds posés avant l'existence
  des blocs (block_id NULL) sont rattachés, à la première ouverture de
  l'onglet, à un "Bloc principal" auto-créé (screens/flow/blocks/
  list_flow_blocks.py) — aucun script de migration séparé, aucune
  donnée perdue.
- Suppression d'un bloc = cascade complète (bloc + tous ses nœuds/
  arêtes), patron identique à screens/custom_events/delete_custom_event.py
  mais scopé à un seul bloc plutôt que game-wide.
- Routes CRUD sous routes/flow_blocks/, montées comme routes/custom_events/.
- templates/screen_edit.html : FLOW (global unique) renommé en ALL_FLOW
  (toutes les données de l'écran) ; un seul bloc ouvert à la fois
  (modale unique, à la Unity) — currentBlockNodes()/currentBlockEdges()
  filtrent ALL_FLOW par CURRENT_BLOCK_ID à chaque rendu, sans tenir de
  seconde copie à synchroniser manuellement.

Vérifié : 215 tests passent (7 nouveaux dans tests/test_flow_blocks.py,
dont un qui verrouille l'ordre d'appel list_flow_blocks()/
list_flow_nodes() dans screen_edit.py — la migration douce doit tourner
AVANT le chargement des nœuds, sinon le compte de nœuds affiché juste
après une migration est périmé), syntaxe JS validée (script de
screen_edit.html rendu via le client de test puis node --check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 11:30:49 +02:00
williamandClaude Sonnet 5 5d6747770c Auto-héberge les polices Google Fonts et animate.css (fin des CDN)
Étape 1 de la fonctionnalité "Publier un jeu en exécutable autonome" :
un jeu exporté devra fonctionner sans AUCUNE connexion internet, ce qui
suppose d'abord que l'app elle-même n'ait plus aucune dépendance CDN —
Bulma l'était déjà (refonte design system), il ne restait que Google
Fonts et animate.css, chargés par templates/play.html ET
templates/screen_edit.html (aperçu du canevas dans l'éditeur).

GOOGLE_FONTS_LINK (screens/widgets/font_options.py) était une constante
FIXE (4 polices, 2 graisses chacune, jamais dépendante du jeu en cours) :
téléchargées une fois pour toutes (45 fichiers woff2, ~1.1 Mo au total
avec la couverture cyrillique/vietnamienne incluse) dans
static/vendor/fonts/, avec un fonts.css régénéré à partir du CSS
officiel de Google Fonts mais pointant vers les fichiers locaux. Même
chose pour animate.min.css 4.1.1 (static/vendor/animate.min.css).

routes/play/game_play.py et routes/screens/screen_edit.py ne passent
plus google_fonts_link aux templates (devenu inutile, les deux pages
chargent directement les fichiers locaux) ; GOOGLE_FONTS_LINK est retiré
de screens/widgets/font_options.py et screens/__init__.py (plus aucun
appelant).

tests/test_animations.py : les deux tests qui vérifiaient la présence
du lien CDN vérifient maintenant la présence du fichier local
(animate.min.css) — renommés en conséquence.

199 tests toujours verts. Reste à faire pour la fonctionnalité complète :
empaquetage Python portable + serveur minimal + bouton "Publier".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 09:29:51 +02:00
williamandClaude Sonnet 5 c60884ec46 Corrige une régression fonctionnelle : revient à Bulma 1.0.2 (plus de Sass)
L'utilisateur a signalé des bugs réels en jeu après la refonte design
system : jauges qui ne se remplissent plus, conditions de visibilité
cassées sur une carte (enveloppe ouverte/fermée affichées en même temps).
Racine du problème : Phase A avait basculé Bulma de 1.0.2 (CDN d'origine)
vers 0.9.4, seule version compatible avec libsass (1.0.2 utilise le
système de modules @use/@forward que libsass ne sait pas compiler). Or
screens/rendering/render_jauge.py pilote le remplissage d'une jauge en
fixant en ligne --bulma-progress-value-background-color — une variable
CSS qui n'existe QUE dans le nouveau système de theming de Bulma 1.x,
absente de 0.9.4. Aucune perte de données : les champs d'objet étaient
toujours intacts en base (vérifié directement sur projects/test/game.db)
— uniquement un problème de rendu/comportement en jeu.

Correction : revient à Bulma 1.0.2, vendoré tel quel et non modifié
(static/vendor/bulma.min.css, ~677 Ko, auto-hébergé — toujours aucune
dépendance CDN). Bulma 1.x expose déjà tout son thème via de vraies
variables CSS (--bulma-primary-h/-s/-l, --bulma-radius...), justement
conçues pour être surchargées après coup SANS recompilation Sass —
styles/bulma-override.css les redéfinit avec la palette Forge (teintes
HSL calculées à partir des couleurs de la charte). build_css.py devient
un simple concaténage de 4 fichiers (bulma.min.css + bulma-override.css
+ forge-tokens.css + forge-custom.css), plus besoin de libsass ni
d'aucun compilateur — supprimé de requirements.txt. styles/bulma/ (source
Sass 0.9.4 vendorée en Phase A) et styles/forge-theme.scss supprimés.

Nouvelle règle ajoutée en commentaire dans bulma-override.css : ne plus
jamais changer de version de Bulma sans `grep -rn "\-\-bulma-" screens/
templates/` d'abord — cette dépendance n'est pas que visuelle.

197 tests toujours verts (ils ne couvrent que le HTML généré, jamais le
rendu réel — c'est pour ça que cette régression n'avait pas été détectée
avant que l'utilisateur ne la signale en jouant pour de vrai).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 08:40:16 +02:00
williamandClaude Sonnet 5 9157b16038 Refonte design system — Phase C : en-tête de page (accueil, tableau de bord)
- index.html ("Mes jeux") : le <h1> nu est remplacé par l'en-tête de page
  compact du §5 (.dashPanelHeader, déjà utilisé ailleurs pour ce
  patron) — titre + badge du nombre de jeux (.forge-badge) sur une seule
  ligne. Pas de second CTA : le formulaire de création reste dans sa
  colonne dédiée, un bouton de plus ferait doublon.
- game_dashboard.html : ajout du même en-tête, absent jusqu'ici (la page
  démarrait directement sur la barre d'onglets) — nom du jeu + chemin du
  dossier, pour savoir sur quel jeu on se trouve sans avoir à regarder
  l'URL.
- styles/forge-custom.scss : .content-objectEdit > h1/.hint/form/
  .warningBanner (règle qui fige la hauteur des enfants directs non
  extensibles dans la mise en page flex de l'accueil) étendue à
  .dashPanelHeader, qui remplace maintenant le <h1> direct.
- profile.html : déjà conforme (rayons de boîte/bouton, danger tokenisé)
  depuis le passage global des tokens en Phase A — aucun changement
  supplémentaire nécessaire.

197 tests toujours verts ; vérification ciblée (fixture jetable, supprimée
ensuite) confirmant l'en-tête et le JS du tableau de bord.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 08:08:55 +02:00
williamandClaude Sonnet 5 62d25dbdce Refonte design system — Phase B : chrome global et pages d'authentification
- styles/forge-custom.scss : .content-wide (déjà utilisée par
  game_dashboard.html/index.html) passe de 1080px à 1600px, pleine
  largeur pour les écrans de gestion (§5) — la largeur du bloc <main> par
  défaut (760px, hérité par les pages d'auth/profil qui ne posent pas
  cette classe) reste inchangée, ces pages veulent justement rester
  étroites et centrées.
- Pages d'authentification (login/register/2FA×2/mot de passe oublié/
  réinitialisation) : le style="max-width:NNNpx" répété sur chacune est
  remplacé par les classes partagées .authScreen/.authCard(-wide), titre
  H1 en dégradé signature .forge-gradient-title — seul endroit du site,
  avec le logo, autorisé à l'utiliser (§3/§5). Grille de fond fine sur
  body.authBody, elle aussi réservée aux zones hero et absente des écrans
  de travail.
- Logique JS inchangée (jauge de mot de passe dans register.html/
  reset_password.html) — uniquement des classes/structure autour.

197 tests toujours verts, JS des pages modifiées revérifié (node --check).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 08:05:27 +02:00
williamandClaude Sonnet 5 f2a73f2c73 Refonte design system — Phase A : Bulma auto-hébergé compilé via Sass
Met en place les fondations du template Bulma décrit dans
regles/FORGE_ENGINE_TEMPLATE_BULMA.md : palette orange/ambre Forge
(--accent:#ff5f2e/--accent-2:#ffb020), typographie, rayons — appliqués
en recompilant Bulma lui-même plutôt qu'en le surchargeant après coup en
CSS, pour recolorer automatiquement ses composants internes (tags,
notifications, dropdowns...) sans avoir à les surcharger un par un.

- Bulma 0.9.4 vendoré en local (styles/bulma/, source Sass classique
  $variable + @import) — PAS 1.0.2 (la version jusqu'ici en CDN) : 1.0.2
  utilise le nouveau système de modules @use/@forward, que libsass (choisi
  pour rester 100% Python, sans Node/npm) ne sait pas compiler (testé :
  il ignore silencieusement le @use au lieu de le traiter). 0.9.4 est la
  dernière version compatible avec libsass et couvre à l'identique tous
  les composants utilisés ici (boutons, tableaux, onglets, modales,
  formulaires, navbar).
- requirements.txt : +libsass (pip pur, aucun binaire/Node.js).
- styles/forge-theme.scss (nouveau, point d'entrée) : variables Sass
  Bulma ($primary, $radius...) posées avant l'import, tokens Forge exposés
  en :root (--forge-bg, --accent, --gradient, --status-*...) avec des
  alias vers les noms de variables déjà utilisés par tout le CSS custom
  existant (--bg/--panel/--border/--text/--danger...) — pas besoin de
  renommer les ~600 lignes de règles déjà écrites, seules leurs VALEURS
  changent.
- styles/forge-custom.scss : ancien static/style.css, structurellement
  inchangé — seuls les hex/rgba en dur qui échappaient aux variables
  (ancien accent bleu #5b8cff, danger #e2685f, couleurs de types de
  nœuds du graphe de logique, fond du QR code recovery...) sont
  remplacés par les tokens de la charte.
- build_css.py (nouveau) : compile styles/forge-theme.scss en
  static/style.css via libsass — un seul fichier, un seul <link>
  inchangé dans les templates, à relancer manuellement après toute
  modification sous styles/.
- base.html/play.html : suppression du <link> CDN Bulma (auto-hébergé
  désormais), ajout d'un favicon (absent jusqu'ici) et du vrai logo Forge
  dans la navbar (assets/*.svg copiés dans static/branding/, seul dossier
  réellement servi par Flask).

197 tests toujours verts (aucune assertion sur des valeurs CSS).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 08:01:32 +02:00
williamandClaude Sonnet 5 dedbda422c Rend la page de profil compacte et sur 2 colonnes
Les 5 sections (email, informations, mot de passe, codes de récupération,
suppression du compte) tenaient les unes sous les autres dans une seule
colonne étroite (560px) avec le padding/espacement par défaut de Bulma —
imposait de scroller pour tout voir. Réorganisé en grille CSS 2 colonnes
(container élargi à 980px, static/style.css : .profileGrid/.profileCol/
.profileBox) : compte/infos/suppression à gauche, sécurité (mot de passe,
codes de récupération) à droite. Champs resserrés (labels remplacés par
des placeholders, is-small partout, marges réduites), jauge de force du
mot de passe sur une ligne (.passwordChecklist-compact). Repasse en une
seule colonne sous 720px de large.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 06:46:19 +02:00
williamandClaude Sonnet 5 9efe119936 Ajoute des codes de récupération 2FA (perte du téléphone)
10 codes à usage unique (format "xxxx-xxxx-xxxx") sont générés à
l'instant même où la 2FA est confirmée (auth/recovery_codes.py, table
_recovery_codes séparée pour marquer/consommer chaque code un par un) —
seul leur hash (werkzeug, comme les mots de passe) est stocké, ils ne
sont visibles en clair qu'à cet instant précis.

Plutôt que d'interrompre la redirection habituelle après confirmation de
la 2FA, les codes sont posés en session ("recovery_codes_to_show") et
affichés une seule fois, en modal, dès le premier rendu de base.html qui
suit (core/recovery_codes_flash.py, session.pop) — préserve tel quel le
comportement de redirection déjà couvert par les tests existants.

Sur /login/2fa, un code de récupération est accepté à la place du code
TOTP habituel (routes/auth/login_2fa.py) : verify_totp est essayé en
premier (verify_recovery_code consomme le code dès qu'il correspond, on
ne veut pas en griller un pour rien sur une saisie qui aurait en fait
été un TOTP valide). Compte toujours vers le même compteur anti-bruteforce
que le code TOTP (déjà en place, voir auth/rate_limit.py).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 20:42:39 +02:00
williamandClaude Sonnet 5 a52b244f27 Ajoute la protection CSRF sur tous les formulaires et requêtes AJAX
Un jeton unique par session (core/csrf.py, exposé côté Jinja via
csrf_token()) est vérifié sur toute requête non-GET par un before_request
(core/csrf_guard.py), dans le même esprit que core/auth_guard.py : une
seule garde globale plutôt que de toucher aux ~90 routes existantes une
par une.

L'app entière fait déjà transiter ses formulaires par fetch() : pjax.js
intercepte chaque <form> interne et le transforme lui-même en requête
fetch (aucun usage de l'attribut d'échappement data-no-pjax nulle part
dans le repo, confirmé par grep). Il suffit donc de patcher window.fetch
UNE SEULE FOIS (static/csrf_fetch.js) pour y ajouter automatiquement
l'en-tête X-CSRFToken sur toute requête non-GET, formulaires pjax comme
fetch() écrits à la main dans screen_edit.html/game_dashboard.html/
play.html — sans modifier un seul appel existant.

La vérification est désactivée quand app.config["TESTING"] est actif
(même convention que Flask-WTF/WTF_CSRF_ENABLED), pour ne pas avoir à
ajouter le jeton aux ~170 tests existants qui appellent les routes
directement via le client de test Flask. tests/test_csrf.py réactive
volontairement la garde pour la mettre à l'épreuve pour de vrai (GET
jamais bloqué, POST sans jeton/avec mauvais jeton -> 400, POST avec le
bon jeton via l'en-tête ou le champ de formulaire -> succès).

templates/play.html reçoit les mêmes deux balises que base.html car il
est autonome (ne l'étend pas, propre <html>/<head>).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 20:35:32 +02:00
williamandClaude Sonnet 5 d90abc827b Ajoute l'authentification : inscription, mot de passe fort, 2FA obligatoire, isolation par utilisateur
Première des deux grandes fonctionnalités demandées (authentification
d'abord, export HTML/CSS/JS autonome ensuite) :

- Inscription (nom, prénom, email UNIQUE, mot de passe) avec schéma
  visuel du mot de passe (jauge + liste de critères qui passent au vert
  en direct — auth/password_strength.py, mêmes règles vérifiées côté
  serveur qu'affichées côté client).
- Double authentification (TOTP, compatible Google Authenticator/Authy)
  OBLIGATOIRE dès l'inscription : QR code (SVG, sans dépendance Pillow)
  à scanner puis code à confirmer avant que le compte soit utilisable —
  voir auth/create_user.py (totp_confirmed) et routes/auth/register_2fa.py.
- Connexion en 2 temps (mot de passe puis code TOTP), déconnexion.
- Isolation par utilisateur : un compte "user" est limité à un SEUL
  projet, dont le dossier est nommé d'après son adresse email (slugifiée)
  et créé automatiquement dès la 2FA confirmée — aucune page de gestion
  multi-jeux pour lui (redirigé directement vers son propre tableau de
  bord). Le rôle "admin" reste illimité, comme le moteur l'a toujours été
  (le TOUT PREMIER compte jamais créé sur une base de comptes vide devient
  automatiquement admin — voir auth/is_first_user.py — pas de mot de
  passe par défaut à faire circuler : s'inscrire en premier suffit).
  Un compte "user" ne peut pas non plus supprimer son unique projet
  (aucune façon d'en recréer un ensuite).
- Garde d'accès globale (core/auth_guard.py, un seul before_request) :
  toute page exige une connexion, sans avoir touché individuellement aux
  ~80 routes déjà existantes du moteur.

tests/conftest.py isole complètement les tests de la vraie base de
comptes (FORGE_USERS_DB_PATH/FORGE_SECRET_KEY_PATH vers un dossier
temporaire propre à la session de tests) et authentifie automatiquement
la fixture `client` partagée en tant que compte admin de test — les 155
tests déjà existants continuent de passer SANS AUCUNE modification de
leur côté, exactement comme avant l'authentification. 10 nouveaux tests
dédiés (tests/test_auth.py) : inscription/mots de passe/2FA/connexion/
isolation par projet/blocage de suppression, vérifiés en conditions
réelles (vrai client de test Flask, vraie base SQLite, vrais codes TOTP
calculés avec pyotp). 165 tests au total, tous au vert.

Nouvelles dépendances : pyotp, qrcode (requirements.txt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 19:30:43 +02:00
williamandClaude Sonnet 5 8cffbeac68 Donnée liée : conditions ET/OU illimitées + conditions de logique sur une variable globale
Deux fonctionnalités demandées, développées et corrigées dans cet
échange :

1. "Donnée liée" (Texte/Titre) : le réglage à 2 filtres fixes (toujours
   combinés en ET) devient une liste de conditions ILLIMITÉE, avec un
   choix ET/OU pour les combiner (screens/widgets/controls/c_clause_list.py,
   screens/clause_list_codec.py). Rétrocompatible avec les anciens
   éléments (_data_filtre_champ/_data_filtre2_champ), convertis à la
   volée à la lecture, sans migration. Après un premier essai à la
   présentation trop compacte et technique (retour utilisateur : "pas de
   champ technique, pas de notation bizarre {{ }}"), la présentation
   finale reprend EXACTEMENT l'ancien style (labels "Champ"/"...est"/
   "...cette valeur", même sélecteur de valeur fixe/dynamique/variable
   déjà existant, jamais la syntaxe brute), simplement répétée par
   condition (templates/partials/clause_row.html), avec un bouton
   "+ Ajouter une condition" bien visible et une liste scrollable
   (static/style.css, .clauseListWrap). Le même moteur (filter_repeater_
   rows.py généralisé) profite aussi au Répéteur de données en interne.

2. Nœud Condition de la Logique de la scène : peut désormais tester une
   VARIABLE GLOBALE en plus d'un champ d'objet (cond_source/cond_variable/
   cond_variable_chemin — screens/flow/ensure_flow_schema.py), sur la
   clause principale ET chaque clause supplémentaire (ET/OU). Évalué côté
   CLIENT (templates/play.html, evaluateConditionClause), contre un
   nouveau gameData.variables exposé par full_game_payload.py — tenu à
   jour par refreshRuntimeData() après toute action qui modifie une
   variable, sans changement supplémentaire nécessaire. Le panneau de
   condition reste utilisable même sans aucun objet défini dans le jeu
   (avant, il disparaissait entièrement).

Vérifié : 153 tests pytest (nouveaux : test_data_binding_clause_list.py,
test_condition_variable.py) + logique JS d'évaluation des conditions
vérifiée isolément avec Node (variable scalaire, objet avec chemin
chaîné, tableau par index, variable introuvable, booléen, rétrocompatibilité
legacy) + rendu des deux pages (éditeur/jeu) vérifié sur le vrai projet
"test" en plus des jeux de test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:05:02 +02:00
williamandClaude Sonnet 5 1b24df9546 Éditeur à champs pour les variables Objet/Tableau (plus de JSON à taper)
L'édition en JSON brut (textarea) n'était pas adaptée à un public non
développeur. Remplacée par un panneau "Configurer la valeur" (déplaçable/
redimensionnable/plein écran, comme les autres) avec une ligne par clé
(Objet) ou par élément (Tableau) : un champ Clé (Objet seulement), un
type (Texte/Nombre/Oui-Non) et une valeur en texte libre — le JSON est
reconstruit automatiquement au clic sur "OK", jamais tapé à la main.

Un seul panneau partagé (#varValuePanel) pour toutes les variables : à
l'ouverture (openVarValuePanel(targetInputId, mode)), reconstruit une
ligne par entrée déjà présente dans le JSON actuel de l'<input> ciblé —
un objet/tableau imbriqué en valeur est préservé tel quel (JSON.stringify
en texte opaque) plutôt que perdu, même si cette valeur précise n'est pas
éditable via une ligne (portée volontairement limitée aux valeurs
simples : suffisant pour une configuration ou une liste, sans la
complexité d'un éditeur JSON récursif).

- Barre "+ Créer une variable" : le champ de valeur devient en lecture
  seule pour Objet/Tableau, avec un bouton "🔧 Configurer" à côté.
- Ligne d'une variable existante : la cellule Valeur montre un aperçu en
  lecture seule + le même bouton "🔧 Configurer" (au lieu du textarea
  JSON du commit précédent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:41:37 +02:00
williamandClaude Sonnet 5 b21f56e364 Filtre de colonnes + panneau de détail d'une entrée (onglet Données)
Un objet avec beaucoup de champs rendait le tableau "Données
enregistrées" illisible (toutes les colonnes serrées sur une ligne,
valeurs tronquées) — deux ajouts pour compenser :

1) "🔧 Colonnes" : menu déroulant (une case à cocher par champ, toutes
cochées par défaut) au-dessus du tableau — décocher un champ masque sa
colonne (th + td, via [data-col]) sans reconstruire le tableau. Un seul
menu ouvert à la fois, fermé au clic ailleurs sur la page.

2) 👁️ par ligne : ouvre un panneau de détail (déplaçable/redimensionnable/
plein écran par défaut, comme les autres) listant TOUS les champs de
cette entrée, un par ligne, valeur complète non tronquée — reprend le
style .rowDetailField/.rowDetailLabel/.rowDetailValue de l'ancienne
modale Bulma de data_list.html (retirée, mais ce CSS ne dépendait pas du
template et a été gardé). Un seul panneau PAR OBJET (pas par ligne) :
son contenu est reconstruit à l'ouverture en lisant en direct la ligne
du tableau déjà affiché (via [data-row-id] sur chaque <tr>) — reflète
donc aussi une modification tout juste saisie mais pas encore
"Enregistrer"ée, sans aller-retour serveur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:17:10 +02:00
williamandClaude Sonnet 5 943bf265de Panneaux flottants en plein écran par défaut, avec bouton pour en sortir
Chaque panneau (Nouvel objet, Modifier un objet, Ajouter un champ,
Ajouter une entrée) s'ouvre désormais en plein écran par défaut (marge
de 12px) plutôt qu'en petite fenêtre centrée — plus confortable dès
qu'il y a plusieurs champs/entrées à voir en même temps. Un bouton ⛶
dans l'en-tête bascule vers/depuis la taille et la position précédentes
(mémorisées le temps de la session, pas persistées), pour qui préfère
un panneau plus petit à côté du reste du tableau de bord.

makeFloatPanelFullscreenable(panel, toggleBtn) — générique, posée sur
panel._fsToggle — appelée par les 4 familles de panneaux existantes.
makeFloatPanelDraggable()/makeFloatPanelResizable() sortent d'abord
proprement du plein écran si l'utilisateur interagit manuellement
(glisser l'en-tête ou tirer le coin) : sans ça, bottom/right encore
actifs en plein écran auraient repris la main sur la position/taille
qu'on vient de poser.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 13:06:58 +02:00