Commit Graph
42 Commits
Author SHA1 Message Date
williamandClaude Sonnet 5 d664ed5637 Remplace le système de quêtes par des déclencheurs, ajoute l'action variable et le chaînage
Build and deploy / test-python (push) Successful in 8m45s
Build and deploy / test-js (push) Successful in 49s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Supprime le concept de "quête" au profit d'un onglet unique "Déclencheurs"
portant toute la logique (dialogue, condition, marquage terminé) directement
sur l'objet de scène. Ajoute une nouvelle action "Modifier une variable"
(réutilisant le vocabulaire du graphe de flow) utilisable après une
collision, une interaction ou une branche de condition, ainsi qu'un
chaînage d'actions ("then") permettant d'enchaîner plusieurs actions à la
suite et d'étendre un déclencheur déjà posé sans le recréer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 22:05:06 +02:00
william 50835a18e2 Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 1m0s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Le document de cadrage produit cible des formateurs non techniques créant
des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de
logique en nœuds, timeline d'animation, définitions d'objets/relations,
templates réutilisables) est une complexité hors cible que l'effort
d'ingénierie récent avait déjà abandonnée au profit du jeu_2d.

- Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire
  Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only)
- Suppression en bloc des modules exclusifs au document : routes/elements,
  routes/element_types, routes/objects, routes/legacy_actions,
  screens/elements, screens/element_types, screens/widgets,
  screens/legacy_actions, le rendu render_element_html.py et son cluster,
  templates/screen_edit.html, templates/game_dashboard.html,
  flow-editor.js/tabs-and-blocks.js/animation-timeline.js
- Dashboard toujours simplifié (un seul mode possible désormais)
- Tests document-only supprimés, tests de logique partagée (flow,
  événements personnalisés, animations) retargetés sur des écrans jeu_2d
- Aucune régression jeu_2d : 299 tests passent

Carte d'onboarding retravaillée : argumentaire RH non technique (liste à
coche, badge "Compatible LMS"), taille et interaction de retournement
ajustées.
2026-09-04 20:56:32 +02:00
williamandClaude Sonnet 5 521792fe00 Dialogues de quête : bulles illimitées, n'importe quel objet, timeline reliée
Build and deploy / test-python (push) Successful in 7m32s
Build and deploy / test-js (push) Successful in 1m2s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
- "qui parle" n'est plus réservé au personnage : N'IMPORTE QUEL objet de
  scène nommé (personnage, décor, fond — voir "ℹ️ Informations",
  screens/rendering/scene_object_names.py, ex-personnage_names.py) peut
  parler dans un dialogue.
- Confirmé/documenté : aucune limite au nombre de répliques par colonne
  (bouton "+ Réplique" reste toujours disponible).
- Bulle = header (menu déroulant du "qui parle") + body (le texte),
  centrée dans sa colonne à 70% de largeur, alignée verticalement et
  reliée à la suivante par un trait — remplace l'ancien alignement
  gauche/droite façon chat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:54:06 +02:00
williamandClaude Sonnet 5 2347a70c45 Quêtes/collision : personnages nommés, dialogues qui parlent, boîte de dialogue en jeu
Build and deploy / test-python (push) Successful in 11m32s
Build and deploy / test-js (push) Successful in 1m12s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
- "ℹ️ Informations" (propriétés d'un personnage) : nom éditable, réutilisé
  comme "qui parle" dans l'éditeur de dialogue de quête (menu déroulant,
  plus "Joueur" toujours disponible) — remplace l'ancien choix binaire
  joueur/pnj. db.sanitize_quest_dialogues accepte maintenant un nom libre.

- Nouveau menu "🖥️ Interface" (palette d'objets) avec le premier widget :
  "💬 Boîte de dialogue" (kind="dialogue_box", screens/rendering/
  dialogue_box_style.py) — position/taille comme tout objet de scène,
  panneau "🎨 Style" dédié (police, taille, épaisseur, couleur du texte,
  couleur header/body/footer). Rendu en <div> à 3 zones, jamais soumis à
  la collision ni à l'éditeur de collision (exclu partout : obstacles,
  liste des règles, payload). Rendu côté jeu dans .sceneUI, une couche
  FIXE au viewport (jamais .sceneWorld, qui défile avec la caméra).

- static/js/play/dialogue-box-controller.js : fait le lien moteur entre
  l'action "quete" de l'éditeur de collision et ce widget — affiche la
  réplique en cours du dialogue correspondant au STATUT ACTUEL de la
  quête (gameData.quests, désormais exposé game-wide par
  full_game_payload.py), avance au clic sur "Suivant" (header = qui
  parle, body = texte, footer = bouton), se masque à la dernière réplique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 11:20:58 +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 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 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 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 e82eb7ce88 Retire l'export exécutable (.exe) et la partie publique en ligne (/jouer)
Build and deploy / test-python (push) Successful in 7m5s
Build and deploy / test-js (push) Successful in 58s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Décision produit : seul l'export Web/SCORM (LMS) est pertinent — l'export
exécutable Windows autonome (publish/build_package.py, bouton "Publier")
et la partie publique par joueur (/jouer/<slug>, routes/public_play/,
"Publier en ligne") sont jugés redondants et retirés.

Conserve le mécanisme d'état "par joueur" (db/global_vars,
db/rows, per_player) : infrastructure générique déjà utilisée par
Score/Progression et testée indépendamment de toute route publique
(voir tests/test_player_state.py), aucune raison de la retirer.

_STATIC_ITEMS/_copy_characters (copie sélective des sprites CraftPix
réellement utilisés) migrent de build_package.py vers
build_scorm_package.py, seul appelant restant.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 10:59:28 +02:00
williamandClaude Sonnet 5 a27d08c6b0 Bibliothèque de sprites animaux CraftPix (2/3) : export .zip sélectif
Build and deploy / test-python (push) Failing after 1m50s
Build and deploy / test-js (push) Successful in 7s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Corrige aussi un import cassé du commit précédent : screens/__init__.py
importe déjà screens.rendering.list_used_forge_characters (nécessaire à
screens.ADMIN_ONLY_CHARACTER_SLUGS/la galerie filtrée), mais ce fichier
n'avait pas été inclus dans ce commit — screens/ ne s'importait plus en
l'état. Corrigé ici, dans le même commit que sa seule vraie utilisation.

publish/build_package.py copiait jusqu'ici static/characters/ dans SON
INTÉGRALITÉ pour CHAQUE jeu exporté (.zip), sans filtrage — avec la
bibliothèque d'animaux (~2,1 Go), ça aurait gonflé chaque export du poids
de toute la bibliothèque, même un jeu qui n'utilise aucun animal.

screens.list_used_forge_characters(slug) (nouveau) parcourt tous les
écrans du jeu (écrans-modèles compris, même patron que
full_game_payload.py) et renvoie l'ensemble des personnages Forge
réellement référencés. _copy_engine_sources()/_copy_characters()
copient désormais : le rangement plat Kenney (petit, comme avant) +
toujours static/characters/animals/manifest.json (nécessaire à l'IMPORT
de screens/labels/animal_sprite_library.py dans le paquet exporté,
sinon le jeu exporté ne démarre plus) + UNIQUEMENT les dossiers
animal/variante réellement utilisés par CE jeu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:52:13 +02:00
williamandClaude Sonnet 5 449c36fd5d Bibliothèque de sprites animaux CraftPix (1/3) : catalogue, galerie, accès admin
Build and deploy / test-python (push) Failing after 12s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Premier commit d'une fonctionnalité découpée en plusieurs lots (voir le
plan "Bibliothèque de sprites animaux CraftPix") : intègre 14 familles
d'animaux (15 variantes de couleur chacune) comme personnages Forge
sélectionnables, à côté des 6 Kenney existants — réservé au rôle admin,
licence CraftPix oblige (interdiction contractuelle de rendre ces sprites
utilisables par un compte "user" via l'application).

- screens/labels/animal_sprite_library.py (nouveau) : charge un manifest
  JSON généré une fois (voir scripts/generate_animal_sprite_manifest.py,
  commit suivant) et construit ADMIN_SPRITE_LIBRARY, dans le même format
  que l'existant PUBLIC_SPRITE_LIBRARY (screens/labels/sprite_library.py,
  ex-SPRITE_LIBRARY, renommé pour distinguer les deux). screens.SPRITE_LIBRARY
  reste le catalogue FUSIONNÉ (utilisé par resolve_personnage_animations
  pour la résolution runtime, sans filtrage par rôle — voir le constat
  d'exploration : le payload de jeu et /jouer/<slug> ne vérifient déjà
  aucun rôle nulle part).
- screens/labels/sprite_gallery.py (nouveau) : sprite_gallery_families()
  groupe la galerie par famille — un animal n'apparaît qu'une fois (sa
  variante "de base"), ses 15 couleurs se choisissent depuis le panneau
  de propriétés (render_variant_gallery, templates/screen_edit.html),
  répondant à la suggestion de l'utilisateur plutôt que d'encombrer la
  galerie d'ajout de 210 tuiles quasi identiques.
- routes/screens/screen_edit.py, routes/scenes/scene_edit_view.py :
  la galerie passée au template est filtrée par rôle
  (PUBLIC_SPRITE_LIBRARY pour un compte "user", SPRITE_LIBRARY complet
  pour un admin) — même idiome que core/auth_guard.py.
- core/sprite_gate.py (nouveau) + 4 routes d'écriture (element_add,
  element_set_personnage_data, scene_object_add, scene_object_personnage_data) :
  ferme la brèche d'un POST direct qui contournerait la galerie filtrée
  (403 si un compte non-admin tente d'assigner un personnage animal).
- tests/conftest.py : nouvelles fixtures user_client/user_game (compte
  "user" non-admin avec un projet assigné) pour tester le filtrage par
  rôle de bout en bout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-31 21:49:05 +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 236d6b4b46 Phase 1 (1/3) : état par joueur — couche db/ et chaîne de rendu
Build and deploy / test (push) Failing after 8s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Fil directeur du plan : chaque variable globale et chaque objet de
données gagne un player_id (sentinelle PLAYER_SHARED='__shared__' par
défaut partout — l'aperçu créateur et tous les tests existants
continuent de fonctionner À L'IDENTIQUE, aucune signature appelée sans
argument explicite ne change de comportement), plus un réglage
per_player choisi une fois à la création :
- per_player=1 (par défaut) : chaque joueur a sa propre valeur/ses
  propres lignes.
- per_player=0 : valeur/lignes partagées par tous les joueurs (ex. un
  compteur de visiteurs global, un catalogue commun).

db/global_vars/ : _global_variables passe de UNIQUE(name) à
UNIQUE(name, player_id) — SQLite ne permet pas de modifier une
contrainte UNIQUE via ALTER TABLE, reconstruction de la table détectée
et faite une seule fois (ensure_global_vars_schema.py) pour les jeux
créés avant cette phase. La ligne "modèle" (player_id=PLAYER_SHARED,
créée par le créateur) porte le réglage per_player et sert de valeur
PAR DÉFAUT : la première écriture d'un joueur sur une variable
per_player crée paresseusement SA propre ligne (copiée depuis le
modèle) ; une lecture sans ligne encore écrite retombe sur le modèle
(nouveau resolve_player_key.py). list_global_variables() (tableau de
bord) ne montre toujours que les lignes modèles ; nouveau
list_global_variables_for_player() expose la valeur EFFECTIVE d'un
joueur au runtime (full_game_payload.py).

db/definitions/ + db/rows/ : chaque table d'objet généré
(create_definition.py) gagne une colonne player_id (ADD COLUMN simple,
pas de contrainte UNIQUE en jeu ici) ; _definitions gagne per_player.
Contrairement aux variables, PAS de repli sur une ligne "modèle" pour
les lignes d'un objet per_player — une LISTE n'a pas de valeur par
défaut unique à copier comme un scalaire, un nouvel objet per_player
démarre VIDE pour chaque joueur (nouveau resolve_row_player_key.py).
get_row/update_row/update_row_field/delete_row filtrent aussi par
player_id (pas seulement id) : garde-fou contre un row_id d'un AUTRE
joueur, nécessaire dès qu'un objet per_player sera exposé sur la future
route publique /jouer/<slug>. Migration : nouveau
ensure_player_id_column(slug, table_name), appelé avant toute requête
sur une table d'objet créée avant cette phase.

Chaîne de rendu (screens/elements/list_elements.py ->
screens/rendering/render_element_html.py -> render_repeater.py/
render_jauge.py/resolve_bound_row.py/visibility_condition.py/
filter_repeater_rows.py) : player_id transite dans le ctx déjà utilisé
partout pour "champ en cours" (ctx["_forge_player_id"], même patron que
ctx["_forge_play_mode"], posé une seule fois par list_elements quand
enforce_visibility=True) — pas de nouveau paramètre positionnel à
threader dans chaque fonction, juste une clé de plus dans un mécanisme
déjà en place.

Nouveau tests/test_player_state.py : verrouille à la fois le nouveau
comportement (deux joueurs => valeurs/lignes indépendantes ; per_player=0
=> partagé ; nouveau joueur => valeur par défaut pour une variable, liste
VIDE pour un objet ; get_row ne fuite jamais vers un autre joueur) et la
non-régression de l'aperçu créateur (comportement historique inchangé).

Vérifié : 232 tests passent (9 nouveaux). Reste à faire (prochains
commits) : route publique /jouer/<slug>, identité visiteur (cookie),
bascule "Publier en ligne" dans le tableau de bord.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 15:31:08 +02:00
williamandClaude Sonnet 5 124f250d2b Corrige le contenu de "Donnée liée" figé après un changement de variable/donnée
Bug rapporté : changer une variable globale utilisée comme valeur de
comparaison d'une "Donnée liée" (Texte/Titre, dans une boîte de dialogue
posée comme élément de jeu réutilisable) recalculait bien, côté SERVEUR,
la bonne ligne à afficher — mais le contenu affiché en jeu restait figé
sur son ancienne ligne tant que la page n'était pas complètement
rechargée.

Cause : templates/play.html ne régénère, après une action "Modifier une
variable"/"Modifier une donnée", QUE les éléments dont le rendered_html
porte un marqueur connu (voir refreshRuntimeData()/hasMarker() —
"repeaterItem", "jaugeBar", "visibilityGated"). Un Texte/Titre "Donnée
liée" n'en portait AUCUN : jamais identifié comme "dépendant de la
donnée", donc jamais régénéré, même si le nouveau HTML était déjà prêt
côté serveur à chaque rendu.

Correctif : nouveau marqueur "dataBound" (render_element_html.py), posé
dès que _data_definition_id est réglé, reconnu par hasMarker(). Un
exemplaire d'élément de jeu (ex. la boîte de dialogue posée sur une
scène) porte ce marqueur EN PROFONDEUR dans son propre rendered_html dès
qu'un de ses descendants internes en a un — il se retrouve donc bien
régénéré dans son ensemble, sans changement supplémentaire nécessaire.

Deux nouveaux tests, confirmés en échec sur l'ancien code (même scénario
que le rapport : reproduit avec objet "dialog" + variable "dialog_order"
+ élément de jeu réutilisable) puis au vert avec le correctif. 155 tests
au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 09:47:12 +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 7c237d6f1c Retire le voile plein écran et le forçage de visibilité de l'éditeur (retour utilisateur)
Deux retours après le dernier correctif (78a373a, qui forçait
"display:flex" dans l'éditeur pour que la boîte de dialogue reste
visible) :
- "je souhaite avoir la main sur la visibilité de la modale sinon elle
  s'affiche toujours sur la scène, court-circuite ma logique" — le
  forçage empêchait de vraiment utiliser "Visibilité" pendant l'édition.
- "quand j'édite la modale ou quand je la mets dans une scène je
  souhaite que rien ne soit assombri, l'assombrissement ne se fait que
  quand la scène est jouée" — le voile plein écran (position:fixed +
  fond assombri) restait aussi actif dans l'éditeur.

Correctif (render_overlay.py) : le voile plein écran ET le forçage de
visibilité sont retirés de l'ÉDITEUR — ce widget s'y comporte maintenant
comme un CONTENEUR NORMAL (position/taille selon x/y/width/height, aucun
voile, réglage "Visibilité" respecté normalement, comme n'importe quel
autre widget masqué). Le comportement plein écran/voile/masquage par
défaut n'est conservé qu'en mode JOUABLE (ctx["_forge_play_mode"]).

En creusant pourquoi "Visible" ne suffisait pas à faire réapparaître la
boîte dans l'éditeur (menant l'utilisateur à essayer "Invisible" à la
place, visible dans ses captures), trouvé un vrai bug latent dans
save_element_controls.py : l'option "Visible" du réglage "Visibilité" ne
touche volontairement jamais "display" (pour ne pas écraser le
"display:flex" d'un conteneur en disposition ligne/colonne — voir
visibility_control.py) — ça fonctionne seulement parce que, pour un
widget AVEC un réglage "Disposition interne", celui-ci réaffirme lui-même
un display non-"none" au même enregistrement. La "superposition" n'a PAS
ce réglage : "display:none" (posé à la création ou par un "Masqué"
précédent) restait donc bloqué pour toujours, quel que soit le nombre de
fois où "Visible" était ensuite choisi. Corrigé : "Visible" efface aussi
"display" pour tout widget SANS réglage "Disposition interne" (safe : les
widgets qui EN ont un ne sont pas concernés, donc aucune régression sur
leur comportement existant).

Tests mis à jour (l'ancien test attendait le forçage, désormais retiré) +
nouveau test qui couvre le cycle complet (masqué par défaut -> "Visible"
choisi -> apparaît sans voile dans l'éditeur -> voile plein écran
retrouvé en mode jouable). 140 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 07:14:51 +02:00
williamandClaude Sonnet 5 6e46949cf8 Corrige la boîte de dialogue posée comme élément de jeu réutilisable : conteneur vide au lieu du dialogue
Bug rapporté : poser un élément de jeu ("Mes éléments de jeu") dont le
modèle n'est qu'une "Superposition / boîte de dialogue" sur une vraie
scène affichait un conteneur vide à l'endroit du dépôt — jamais le
dialogue. La boîte de dialogue existait bien (masquée comme prévu, en
attente d'une action qui l'affiche) : c'est l'enveloppe qui l'entoure qui
n'aurait jamais dû être visible.

Cause : tout exemplaire d'élément de jeu est posé avec le widget générique
"conteneur" par défaut (add_element.py, colonne default_widget) — son
contenu réel (le modèle) est rechargé EN DIRECT à l'intérieur
(_render_element_type_children), mais l'enveloppe "conteneur" elle-même
reste une boîte NORMALE, toujours visible, avec sa propre couleur de fond/
bordure et sa position fixe sur le canevas (contrairement à une
superposition posée directement, qui, elle, démarre masquée). Résultat :
une boîte vide et permanente à l'endroit du dépôt, pendant que le vrai
dialogue (démarré masqué, correctement) reste invisible en dessous/
au-dessus tant qu'aucune action ne le déclenche.

Correctif : quand le modèle ENTIER d'un élément de jeu n'est qu'une seule
superposition (screens/element_types/is_overlay_only.py, nouveau), on
court-circuite entièrement l'enveloppe "conteneur" (render_element_html.py)
et on retire aussi le z-index de son cadre de positionnement
(element_style_filter.py, list_elements.py) — même raison que pour une
superposition posée directement (81c31a9) : sans ça, ce cadre reste un
candidat à piéger le z-index:9999 du dialogue rendu à l'intérieur dès
qu'un autre élément de la scène a un z-index plus grand.

Nouveau test, confirmé en échec sur l'ancien code (même "class=\"box\""
fantôme reproduit) puis au vert avec le correctif. 140 tests au vert au
total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 06:58:44 +02:00
williamandClaude Sonnet 5 78a373a536 Corrige la vraie cause : la boîte de dialogue restait invisible DANS L'ÉDITEUR (masquée par défaut)
Après vérification serveur, le texte était bel et bien rendu dans le HTML
(couleur correcte incluse) — donc pas un souci de contenu ni de couleur.
La vraie cause : le widget "Superposition / boîte de dialogue" démarre
MASQUÉ par défaut à sa création (display:none, voir
default_style_for_widget.py — pour ne pas couvrir tout l'écran dès qu'on
le pose). Ce display:none est écrit tel quel dans le HTML aussi bien en
mode jouable QUE dans l'éditeur, puisque le rendu de ce widget est
partagé par les deux. Résultat : toute la boîte (et donc son contenu,
peu importe le texte ou sa couleur) restait invisible dans le CANEVAS DE
L'ÉDITEUR dès l'instant de sa création — impossible d'y voir/positionner
visuellement ce qu'on pose dedans tant qu'on n'a pas pensé à basculer
manuellement "Visibilité" sur "Visible" (puis à y repenser pour la
remettre sur "Masqué" avant de tester en jeu).

Correctif (render_overlay.py) : dans l'ÉDITEUR uniquement (détecté via le
ctx "_forge_play_mode" déjà posé par list_elements.py pour le mode
jouable), un "display:flex" est ajouté en dernier dans le style —
gagnant sur le "display:none" par défaut (CSS : la dernière déclaration
de la même propriété l'emporte). Le mode JOUABLE, lui, continue de
respecter ce réglage normalement (masqué tant qu'aucune action ne
l'affiche).

Nouveau test, confirmé en échec sur l'ancien code puis au vert avec le
correctif : vérifie explicitement que le style se termine par
"display:flex;" dans l'éditeur et par "display:none;" en mode jouable.
139 tests au vert au total. Jeu de démo régénéré.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 06:30:24 +02:00
williamandClaude Sonnet 5 69bafcb5c5 Corrige le texte invisible dans la boîte de dialogue (régression du style Bulma)
Régression du commit précédent (d87d6ad, classes Bulma sur la
superposition) : la classe ".box" impose elle-même une couleur de texte
SOMBRE (pensée pour un fond blanc). Comme aucun widget ne fige de couleur
de texte à sa création (default_style_for_widget.py), un titre/texte posé
dans la boîte SANS couleur personnalisée héritait de ce gris sombre
imposé par Bulma — invisible sur le fond sombre par défaut de cette boîte
de dialogue. Ni erreur serveur ni régression de test visible : juste un
texte "présent mais invisible", exactement ce qui a été rapporté ("j'ai
mis un texte dedans mais il ne se voit pas").

Correctif : la boîte fixe elle-même une couleur de texte claire par
défaut (color:#e8eaf0) dans son propre style inline — l'élément le plus
proche gagne, donc ça écrase la couleur imposée par la classe Bulma, tout
en restant surchargeable : un titre/texte qui personnalise sa propre
couleur (réglage "Couleur du texte") continue de l'emporter normalement.

Nouveau test de régression, confirmé en échec sur le commit précédent
(même style Bulma, pas encore cette couleur) puis au vert avec le
correctif. 138 tests au vert au total. Jeu de démo régénéré.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 06:18:19 +02:00
williamandClaude Sonnet 5 d87d6addce Boîte de dialogue (superposition) stylisée avec les classes Bulma
Le widget "Superposition / boîte de dialogue" gagne les classes Bulma
"modal is-active" (sur le voile plein écran) et "box" (sur la boîte
centrée), en PLUS du style inline existant — jamais à sa place : tout le
positionnement/masquage critique (position:fixed, z-index, display) reste
en inline, qui gagne toujours sur une règle de classe. Si Bulma (chargé
depuis un CDN, voir play.html) ne se charge pas (hors-ligne), la boîte de
dialogue continue de fonctionner exactement pareil — ces classes n'ajoutent
qu'un habillage visuel (ombre, base de police/espacement Bulma) qui se
dégrade sans rien casser.

Pas de ".modal-background" séparé (convention Bulma habituelle) : le
voile semi-transparent est déjà posé en inline sur la même balise
(background:rgba(...)), un second calque tout aussi transparent
par-dessus n'aurait ajouté qu'un assombrissement redondant.

137 tests toujours au vert (changement purement visuel, aucune assertion
cassée) ; jeu de démo régénéré et vérifié (classes bien présentes dans le
rendu).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-28 06:10:17 +02:00
williamandClaude Sonnet 5 81c31a9c49 Corrige la boîte de dialogue (superposition) : un élément posé après elle s'affichait par-dessus
Bug visible sur le jeu de démo : le bouton "Clique-moi !" restait visible
ET cliquable AU-DESSUS du dialogue de bienvenue censé couvrir tout l'écran,
et la boîte de dialogue elle-même s'étirait bord à bord au lieu de rester
une boîte centrée lisible.

Cause (stacking context CSS) : le widget "superposition" ignore x/y/
width/height et pose lui-même position:fixed; inset:0; z-index:9999 sur
SA PROPRE balise (render_overlay.py) — mais le cadre .playElement/
.canvasElement qui l'entoure, PARTAGÉ PAR TOUS LES WIDGETS (filters/
element_style_filter.py), continuait quand même à poser
"position:absolute; z-index:<sa place dans le canevas>" (souvent petit,
ex. 1). Un élément positionné avec un z-index explicite crée un NOUVEAU
contexte d'empilement CSS : le 9999 posé plus profond ne se comparait
alors plus qu'AU SEIN de ce contexte, et perdait face au z-index (plus
grand) d'un élément ajouté APRÈS l'overlay sur le canevas — qui
s'affichait donc par-dessus le dialogue.

Correctif : _element_style ne pose plus aucune position/z-index pour ce
widget (position:static — sa place dans le flux est de toute façon
invisible, son contenu réel étant en position:fixed). Plus de contexte
d'empilement local créé à ce niveau : le z-index:9999 se compare
directement à tous les autres éléments de l'écran, et gagne toujours.

Profité de l'occasion pour donner à la boîte une largeur par défaut plus
raisonnable (render_overlay.py : max-width:min(560px, 90%) au lieu de
90% seul) — sur un écran de jeu large, "90%" donnait une boîte étirée
bord à bord peu lisible comme dialogue ; 560px reste confortable, et 90%
prend toujours le relais sur un écran étroit (mobile/portrait).

Nouveau test de régression (test_overlay_wrapper_does_not_trap_its_own_z_index) :
confirmé en échec sur l'ancien code (git stash), au vert avec le
correctif. 129 tests au vert au total.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 18:43:29 +02:00
williamandClaude Sonnet 5 aaf9954446 Variables globales Objet/Tableau, lisibles via un chemin dans les conditions et filtres
Deux nouveaux types de variable globale (db/constants.py) : "objet" et
"tableau" — valeur stockée en JSON (colonne TEXT existante), avec
validation à la création/modification (db/global_vars/
coerce_structured_value.py) : un JSON invalide retombe sur un défaut sûr
("{}"/"[]") plutôt que de corrompre silencieusement la variable pour
toutes ses lectures suivantes. apply_variable_action.py n'a besoin
d'aucun changement — "definir_texte" écrit déjà n'importe quelle chaîne
telle quelle.

Nouvelle résolution de chemin, partagée (screens/rendering/
filter_repeater_rows.py::_resolve_variable_path) : navigue dans la
valeur JSON d'une variable selon un chemin ".champ"/"[index]" chaînable
(ex. ".arme.degats", "[0].valeur") — ne lève jamais, renvoie None si le
JSON est invalide ou qu'un segment du chemin ne correspond à rien.
Branchée à deux endroits, qui lisaient déjà une variable globale :

- La valeur de comparaison {{$nom_variable}} (filtres de Répéteur ET
  Condition de visibilité, qui partagent le même
  _resolve_filter_value()) accepte maintenant un chemin optionnel :
  {{$perso.nom}}, {{$scores[0]}}. Le sélecteur "Variable globale" du
  panneau de propriétés (screen_edit.html, .filterValueVariable) gagne un
  champ "Chemin optionnel" à côté du choix de variable — même regex
  étendue côté JS (_VAR_REF_RE) que côté Python (_VAR_REF_PATTERN), pour
  que la valeur round-trip correctement à la réouverture du panneau.

- La variable VÉRIFIÉE par une Condition de visibilité en mode "variable"
  (choisie via un <select>, pas la syntaxe {{$...}}) gagne son propre
  nouveau contrôle "Chemin dans la variable" (visibility_condition_
  controls.py) — nécessaire pour comparer un champ d'un Objet ou un
  élément d'un Tableau, pas seulement la variable entière.

game_dashboard.html (onglet Variables) : la valeur par défaut de la barre
de création devient un <textarea> (fonctionne aussi bien pour un JSON
multi-ligne qu'un scalaire court), et la cellule "Valeur" du tableau des
variables existantes devient un <textarea> quand le type est objet/
tableau.

4 nouveaux tests (tests/test_variable_object_array.py) : validation JSON
à la création, filtre de Répéteur avec chemin chaîné, condition de
visibilité avec accès par index de tableau, chemin invalide/absent sans
plantage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 14:28:23 +02:00
williamandClaude Sonnet 5 7d48445443 Ajoute "Variable globale" au sélecteur "Valeur fixe / Donnée d'un autre objet"
Le sélecteur de valeur de comparaison (tout contrôle "..._valeur" : filtre
du Répéteur, Donnée liée, condition de visibilité) proposait déjà une
valeur fixe ou le champ d'un AUTRE objet - manquait la possibilité de
comparer à une variable globale (voir db/global_vars/), qui change elle
aussi en cours de partie mais n'est rattachée à aucun objet précis.

Nouvelle syntaxe interne "{{$nom_variable}}" (le "$" la distingue sans
ambiguïté de "{{Objet.champ}}", qui attend toujours un point) :
_resolve_filter_value (filter_repeater_rows.py) va lire sa valeur actuelle
via db.get_global_variable, comme "{{Objet.champ}}" le fait déjà pour un
champ d'objet. Le panneau de propriétés gagne un troisième mode
"Variable globale" à côté de "Valeur fixe"/"Donnée d'un autre objet",
avec la liste déroulante des variables existantes.

Corrige au passage un bug latent découvert en testant bout en bout : un
Répéteur SANS modèle de ligne (texte brut avec {{champ}}) plantait en
mode jouable avec TypeError - render_repeater.py substitue lui aussi
directement les {{champ}} du ctx dans ce cas (repli), et ce ctx porte
aussi _forge_play_mode (un booléen, voir render_element_html.py) depuis
l'ajout de la condition de visibilité - déjà corrigé pour le chemin
générique (apply_ctx.py) mais pas pour ce chemin séparé.

Le sélecteur de champ pour insérer {{champ}} dans "Contenu" (demandé dans
le même message) existe déjà depuis un tour précédent (voir
insertFieldAtCursor()) - vérifié toujours fonctionnel.

Ajoute tests/test_filter_value_global_variable.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:51:18 +02:00
williamandClaude Sonnet 5 94f1414506 Corrige un bug important : le style de TOUT élément neuf était cassé depuis l'ajout de l'Ombre
Bug remonté ("toutes les propriétés ne sont pas prises en compte") : la
vraie cause n'avait rien à voir avec les éléments de jeu — le contrôle
"Ombre" (voir le tour précédent) a une valeur par défaut composite (un
dict Python, {"x":0,"y":4,...}, pas une chaîne CSS). default_style_for_
widget.py, qui fige les réglages "dont la valeur par défaut a un effet
visuel voulu dès la création" pour chaque widget neuf, n'excluait pas ce
nouveau type de contrôle — il écrivait donc ce dict TEL QUEL (repr Python)
dans le style de CHAQUE élément fraîchement créé depuis ce commit, quel
que soit son widget. Une valeur CSS invalide au milieu du style pouvait
donner l'impression que "plein de propriétés" ne s'appliquaient plus.

Deux correctifs :
1. default_style_for_widget.py exclut maintenant "shadow" du gel à la
   création (même raisonnement déjà appliqué à "color" juste au-dessus :
   la valeur par défaut n'est qu'une suggestion affichée dans le panneau,
   pas un réglage neutre à figer - le neutre CSS est "pas d'ombre").
2. style_string.py ignore désormais toute valeur non scalaire (dict/liste)
   au moment de construire l'attribut style - filet de sécurité pour les
   éléments déjà créés AVANT ce correctif, qui portent encore ce dict figé
   en base et continueraient sinon à s'afficher cassés.

Ajoute deux tests de régression dans test_shadow_controls.py (aucune ombre
au premier rendu d'un élément neuf ; un élément déjà corrompu avant ce
correctif continue de s'afficher normalement).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:54:06 +02:00
williamandClaude Sonnet 5 e9a991ed13 Un exemplaire d'élément de jeu posé sur un écran reste maintenant lié à son modèle
Jusqu'ici, poser un élément de jeu depuis le catalogue ("Mes éléments de
jeu") copiait tout son arbre en base (instantiate_template_tree) : chaque
exemplaire devenait indépendant, y compris de son propre modèle - modifier
l'élément de jeu dans son éditeur n'avait plus aucun effet sur les
exemplaires déjà posés ailleurs.

Change ce comportement pour qu'un exemplaire reste TOUJOURS lié à son
modèle, sur le même principe déjà utilisé par un modèle de ligne de
Répéteur (jamais copié, rechargé en direct à chaque affichage - voir
_load_template_tree/_render_repeater) : add_element.py ne crée plus
qu'UNE SEULE ligne plate (avec sa position/taille propres à cet
exemplaire) au lieu de copier tout l'arbre, et render_element_html.py
recharge le contenu depuis l'écran-modèle à chaque rendu quand
element_type_id est réglé. Modifier l'élément de jeu dans son propre
éditeur met donc à jour tous ses exemplaires déjà posés, sur n'importe
quel écran (y compris ceux placés AVANT ce correctif, qui portaient déjà
element_type_id sur leur ligne de premier niveau), sans avoir à les
retoucher un par un.

Contrepartie assumée (discutée avec l'utilisateur avant ce changement) :
un exemplaire ne peut plus être personnalisé individuellement à
l'INTÉRIEUR (texte, couleur d'un enfant précis...) - seules sa position et
sa taille sur l'écran restent propres à chaque exemplaire. Pour changer le
contenu, il faut désormais passer par l'éditeur de l'élément de jeu
lui-même.

instantiate_template_tree.py, devenu inutilisé, est supprimé.

Ajoute tests/test_element_type_live_instances.py (mise à jour d'un
exemplaire déjà posé, propagation jusqu'à "Jouer", position toujours
indépendante par exemplaire) et met à jour un commentaire de test devenu
obsolète dans test_screens_and_elements.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 15:26:45 +02:00
williamandClaude Sonnet 5 baa3da1035 Corrige la comparaison booléenne : "Oui"/"Non" n'était pas reconnu comme valeur vraie/fausse
Bug remonté avec une "Mail card" (icône enveloppe fermée si is_opened
est à "Non", ouverte si "Oui") : les DEUX variantes s'affichaient (ou
aucune), selon la ligne.

Cause : _compare() (filter_repeater_rows.py, utilisé par la condition de
visibilité, le Répéteur et Donnée liée) ne reconnaissait "1"/"true"/"vrai"
comme valeur vraie pour un champ booléen — jamais "oui", pourtant le SEUL
vocabulaire que l'app affiche elle-même pour ce type de champ partout
ailleurs (voir data_list.html : "Oui" si vrai sinon "Non"). Une valeur de
comparaison fixe tapée "Oui" retombait donc silencieusement à "faux",
et comme l'opérateur et le champ étaient par ailleurs corrects, ça
donnait l'impression que la condition "ne voyait" rien : sur la ligne où
is_opened=faux, les DEUX cartes ("égal à Oui" et "égal à Non", toutes
deux évaluées comme "égal à faux") s'affichaient ensemble ; sur la ligne
où is_opened=vrai, aucune des deux.

Fix : "oui" ajouté à l'ensemble des valeurs reconnues comme vraies,
côté Python (_compare) ET côté JS (compareValues() dans play.html, qui
doit rester alignée — utilisée par les nœuds Condition de la Logique de
la scène), cette dernière au passage rendue insensible à la casse comme
son équivalent Python (elle ne l'était pas du tout).

Ajoute un test de régression dédié à ce cas précis.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:52:27 +02:00
williamandClaude Sonnet 5 a28cc5881e Corrige la condition de visibilité : un widget "special_render" visible ne se rafraîchissait jamais en jeu
Bug remonté avec deux icônes (enveloppe fermée / ouverte) posées
directement sur l'écran, chacune conditionnée sur is_opened : après avoir
ouvert le mail (une action "Modifier une donnée"), les DEUX icônes
restaient affichées en même temps au lieu que l'ouverte remplace la fermée.

Cause : refreshRuntimeData() (play.html) ne réévalue, après une action,
que les éléments dont le HTML porte un marqueur ("visibilityGated"/
"repeaterItem"/"jaugeBar"). render_element_html() posait bien ce marqueur
quand un élément sous condition est actuellement visible - mais seulement
sur le chemin de rendu GÉNÉRIQUE (texte, titre, conteneur...), jamais sur
les 9 widgets "special_render" (Icône, Tableau, Superposition, Onglets,
Case à cocher, Liste déroulante, Groupe de champs, Répéteur, Jauge) - un
élément CACHÉ portait toujours son marqueur (via son placeholder), mais un
élément VISIBLE de ce type non. Résultat : l'icône "fermée", visible au
premier chargement, ne portait aucun marqueur et restait donc figée dans
son état d'origine après toute action suivante, pendant que l'icône
"ouverte" (cachée au départ, donc marquée) se mettait, elle, correctement
à jour - d'où les deux affichées ensemble.

Fix : les 9 branches special_render passent maintenant, elles aussi, par
_mark() comme le chemin générique.

Ajoute un test de régression dédié (icône visible sous condition = doit
porter le marqueur).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:31:50 +02:00
williamandClaude Sonnet 5 ff01b3903c Corrige la condition de visibilité (mode "objet") à l'intérieur d'un Répéteur
Bug remonté : dans une "Mail card" (élément de jeu réutilisable, deux
icônes - enveloppe fermée/ouverte - conditionnées sur le champ is_opened
de l'objet Email) posée dans un Répéteur de données, rien ne s'affichait
jamais correctement.

Cause : is_element_visible() (mode "objet") allait toujours chercher en
base la ligne la plus récente de l'objet ciblé (convention "1 seule ligne
= état de partie", correcte pour une Jauge suivant un état de partie),
sans jamais tenir compte de la ligne EN COURS DE RENDU dans un Répéteur -
donc tous les exemplaires du même modèle de ligne évaluaient la MÊME
ligne (la plus récente de tout l'objet Email) au lieu de chacun la
sienne, et affichaient donc tous exactement le même résultat.

Fix : is_element_visible() reçoit maintenant le ctx de rendu (les
{{champ}} de la ligne en cours, déjà posés par render_repeater.py) et,
si le champ réglé s'y trouve, utilise directement cette valeur plutôt que
d'interroger la base - un exemplaire de Répéteur voit donc bien SA propre
ligne. Hors Répéteur, le comportement (ligne la plus récente de l'objet)
est inchangé.

Ajoute tests/test_visibility_condition.py (mode variable, mode objet hors
Répéteur, absence dans l'éditeur, et ce cas précis dans un Répéteur) -
cette fonctionnalité n'avait jusqu'ici aucun test persistant, seulement
des scripts ad-hoc jetés après vérification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 12:17:40 +02:00
williamandClaude Sonnet 5 c04bc0b926 Ajoute la condition de visibilité et les variables globales
Nouveau panneau "Condition de visibilité" disponible dans les propriétés
de TOUT élément (widget) : permet de masquer un élément en mode jouable
selon deux moyens, au choix -
  - une variable globale (nom + type + valeur, une seule par jeu, stockée
    dans une nouvelle table _global_variables) ;
  - le champ d'un objet de données existant (même convention "état de
    partie" - une seule ligne - déjà utilisée par la Jauge).

Une variable ne servant à rien si elle ne peut jamais changer en cours de
partie, ajoute aussi une nouvelle action de flow "Modifier une variable
globale" (parallèle à "Modifier une donnée"), avec sa propre route
d'exécution serveur et son sous-formulaire dans l'éditeur de logique de
scène. Une variable peut aussi se créer à la volée depuis le sélecteur du
panneau de visibilité, sans quitter les propriétés de l'élément.

La condition n'est évaluée qu'en mode jouable (/game/<slug>/play), jamais
dans l'éditeur, pour que l'élément reste toujours sélectionnable. Un
élément masqué se réévalue en direct après toute action "Modifier une
donnée/variable", via le même mécanisme de rafraîchissement déjà utilisé
par la Jauge et le Répéteur.

Corrige au passage deux bugs découverts en testant bout en bout : (1)
apply_ctx plantait sur le nouveau marqueur interne _forge_play_mode (un
booléen parmi les {{champ}} à substituer, qui attend des chaînes) ; (2)
_compare traitait toute valeur booléenne stockée en chaîne ("0" inclus,
donc toujours vraie en Python) comme vraie - correct pour les champs
d'objet (entiers SQLite) mais faux pour les variables globales (toujours
stockées en texte).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 09:34:10 +02:00
william aa3be503ce Expose relation fields in the champ pickers and fix their column resolution
data_definition_options() used to exclude relation-type fields from its field list entirely, so a filter/binding could never reference "the linked object" of a row — and even when a relation field's clean name was typed manually (as the earlier "level.parcour" example needed), it silently matched nothing: every column lookup for a filter/repeater field used the field's own name, but a relation is actually stored in a "<field>_id" column (see create_definition.py), so the lookup always missed.

Relation fields now appear in the champ dropdowns (Répéteur's filtre_champ/filtre2_champ, Donnée liée's data_filtre_champ/data_filtre2_champ) labeled with the object they point to (e.g. "parcour (→ parcours)"), and a new _field_column() helper in filter_repeater_rows.py resolves the right "<field>_id" column whenever the field turns out to be a relation — used consistently by the filter comparison itself, the "{{Objet.champ}}" dynamic-value resolver, the repeater's row content ({{champ}}), and the Donnée liée row context. Jauge's own champ/champ_nom pickers (which need an actual displayable value, not an id) still exclude relations, both server- and client-side.

Verified end to end: the dropdown shows the relation field with its target-object label, and filtering "level" rows by the clean relation field name "parcour" (not "parcour_id") against a dynamic {{game.current_parcours}} reference now actually matches, alongside the existing "number" filter. Full suite green (89).
2026-08-25 06:48:04 +02:00
william 6d80ed6958 Switch icons from a webfont to self-hosted SVG masks — the webfont was the actual problem
Self-hosting Font Awesome's font files (previous commit) didn't fix it either: every icon in the picker still failed identically. That rules out CDN/network blocking specifically and points to something in the browser blocking custom webfont loading altogether regardless of origin (common with strict anti-fingerprinting protection, e.g. Brave's font blocking or certain privacy extensions — @font-face loading is a well-known fingerprinting vector).

Replaced the whole mechanism: each of the 258 curated icons is now a real downloaded SVG file (static/icons/*.svg, sourced from Font Awesome 5 Free's official SVG package) displayed via CSS mask-image (.icon-svg in style.css) instead of a font glyph. A masked SVG isn't a font resource at all, so it isn't subject to webfont-blocking — and it still colors via the existing "color" style property (background-color:currentColor) and sizes via font-size (1em), so no change to how the widget's other controls work.

The "Icône" widget now stores just the icon's slug (e.g. "trophy") in a dedicated attribute instead of a full CSS class string, rendered by a new special_render (render_icone.py). Updated the add-gallery and the properties-panel picker modal to use the same mask technique for their previews, and element_add.py to validate the slug against the curated list. Removed the now-unused self-hosted Font Awesome CSS/webfont files and the <link> tags — no font dependency left for icons at all.

Verified end to end (gallery renders with real SVG previews, simulated add creates a correctly-attributed element, the SVG file is actually served, the per-element picker reflects the current icon). Full suite green (89).
2026-08-24 20:11:49 +02:00
william a7518355ed Expose every Bulma variant as a choosable option, not just a curated few
Adds a shared bulma_variants.py (color/size option lists) and expands BULMA_*_CONTROLS in bulma_controls.py to cover each component's full modifier set: Bouton (colour incl. white/light/dark/black/text, light-shade toggle, size, rounded, outlined, inverted, static, loading, fullwidth), Titre (Bulma size is-1..is-6 independent of heading level, is-spaced), Jauge (size), Tableau (bordered/fullwidth/striped/hoverable/narrow, now toggleable instead of hardcoded), Onglets (alignment, boxed/toggle/toggle-rounded style, size, fullwidth), and every form field — champ_texte/email/mot_de_passe, zone_texte, liste_deroulante (colour, size, rounded, static).

render_select.py/render_onglets.py/render_jauge.py now redirect the merged "class" from _visible_attrs to the actual Bulma sub-element (the .select wrapper, the .tabs div, the <progress> tag) instead of the outer positioning wrapper, since that's what needs to carry the modifier classes.

Also fixed a real latent bug found while wiring this up: a checkbox control with default=True (e.g. Tableau's "Première ligne = en-tête") was never actually applied on a freshly created element — default_style_and_attributes unconditionally skipped ALL checkbox types at creation, so the panel showed it checked while the element itself had nothing set. Now a checkbox's default=True is frozen at creation like any other meaningful default; default=False (the common case) is unaffected.

Verified end to end: every widget's variant controls save and render the right class tokens (spot-checked titre/tableau/onglets/champ_texte/liste_deroulante/jauge), full suite green (89 passed).
2026-08-24 14:50:44 +02:00
william bb01e9fdf1 Make Jauge, Onglets, and every form-field widget real native Bulma elements
- Jauge now renders a real <progress class="progress"> instead of a hand-built pair of absolutely-positioned divs. The bas/haut color interpolation still works, set via Bulma's own --bulma-progress-value-background-color CSS custom property rather than fighting the class.
- Onglets' tab strip is now genuine Bulma tabs markup (tabs > ul > li, is-active on the li) instead of custom forgeTabBar/forgeTabBtn classes; forgeShowTab (duplicated in screen_edit.html and play.html) now toggles is-active to match.
- Champ texte/email/mot de passe get class="input", Zone de texte gets class="textarea", Case à cocher/Bouton radio's existing <label> wrapper gets class="checkbox"/"radio" (Bulma's own convention — the structure already matched, just needed the class), Liste déroulante is wrapped in Bulma's required <div class="select"> (a bare class on the <select> itself has zero effect in Bulma), Tableau gets class="table is-bordered is-fullwidth" with the per-cell inline borders removed so Bulma's own table styling applies.
- Removed the now-dead .forgeTabBar/.forgeTabBtn CSS.

Updated tests/test_jauge.py and tests/test_onglets_widget.py assertions to match the new markup (value="X" attribute instead of width:X% inline style, --bulma-progress-value-background-color instead of background-color, forgeShowTab(this) marker instead of the removed forgeTabBtn class) — same behavior, different rendering mechanism. Full suite green (89 passed), verified end to end against the real "test" project via the actual /play route.
2026-08-24 14:31:55 +02:00
william 38f33d88d9 Make Bulma the default style engine for Bouton/Titre/Conteneur, layered under existing inline customization
Adds a new "class:" control-target kind (save_element_controls.py, _visible_attrs.py) alongside the existing content/attr/style ones, so a widget can carry CSS classes built from independent named slots (color, size, shape...) without them overwriting each other. Bouton and Titre get their Bulma base class (button/title) via fixed_attrs; Conteneur gets an opt-in "Carte (Bulma)" preset instead of a forced default, since it's also used as an invisible layout wrapper. Bouton's font-size/border-radius sliders no longer freeze their default value into inline style at creation (new c_slider no_freeze flag), so Bulma's own button styling shows through until a user actually customizes it — inline style still wins over any class the moment it's set, exactly like today.

Loads bulma.min.css via CDN in screen_edit.html and play.html, same pattern as animate.css.
2026-08-24 12:54:50 +02:00
william 0e5db97180 Let Texte/Titre elements bind directly to a single row of another object
Adds a "Donnée liée" settings group to Texte/Titre: pick an object, optionally match it to the current game state via 1-2 filter conditions (same engine as the data-repeater's filter, including {{Objet.champ}} cross-references), and use {{champ}} in the text content to show a field from that one matching row. Unlike the data repeater — built for showing a list of rows — this covers displaying a single computed value (e.g. the objective of the level matching the game's current parcours/level) without wrapping it in a repeater.
2026-08-24 11:02:54 +02:00
william f58fb2bc3d Support a second AND condition on the data-repeater filter
Needed to filter a "level" object by both its parcour_id and its number at once (e.g. show the level matching the game's current_parcours AND current_level) — the previous filter only supported a single condition.
2026-08-24 10:48:52 +02:00
williamandClaude Sonnet 5 17fa4cf087 Add an Onglets (Tabs) widget
New widget where each tab is a real "conteneur" element posed as a child
(see screens/elements/add_tab.py) — this reuses everything that already
exists for a normal container (adding a Répéteur/Conteneur/etc. inside via
"Ajouter DANS ce conteneur", renaming to change the tab's visible label,
deleting via the standard trash icon) instead of inventing a separate
storage format for tabs.

The widget's own properties panel gets a dedicated "Onglets" section to
add a tab, rename one, jump to its content, or delete it. Rendering
(render_onglets.py) builds a tab bar + one panel per tab, switched
client-side (forgeShowTab, in both screen_edit.html and play.html) with
only one panel visible at a time.

Distinct from the existing "activer_onglet" flow action (2.3, manual
show-one/hide-siblings) — that stays available for custom show/hide
wiring; this widget is the turnkey version with tab management built in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:42:39 +02:00
williamandClaude Sonnet 5 3039489e39 Fix Jauge name centering and add name appearance controls
Center the name+bar block vertically within the Jauge's own box
(justify-content:center) so it no longer looks pinned to the top once a
name label adds extra content height.

Add appearance settings for the name: position (above the bar, or beside
it on the left), alignment when above (centered or left-aligned), font
family, font size, bold, and italic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 19:23:19 +02:00
william 7e4df3fa3a Fix Jauge bar collapsing to 0px when nested with a name label
A Jauge posée dans un conteneur a une hauteur "auto" (voir
_style_string) — "flex:1 1 auto" seul n'avait alors rien à répartir
(le conteneur flex lui-même n'a pas de hauteur définie), donc le
wrapper de la barre s'effondrait à 0px : seul le nom restait visible,
la jauge elle-même disparaissait. Ajout d'un "min-height" plancher
sur ce wrapper, qui laisse toujours la barre visible dans ce cas tout
en la laissant grandir avec flex:1 si l'élément a une vraie hauteur.
2026-08-23 18:55:44 +02:00
william c9d3069f47 Fix Jauge always reading the object's most recent row
A Jauge could only target a whole object, not a specific record —
three gauges pointing at the same "value" field (e.g. Réputation/
Trésorerie/Confiance in one "jauge" object) all silently showed the
most recent row's value, with no way to tell them apart.

Add "Enregistrement (ligne) à suivre" (row_id) so a Jauge targets one
specific row, and "Champ contenant le nom" (champ_nom) to show a
label above the bar — both as dropdowns populated from the object's
actual fields/rows (previously "Champ numérique à afficher" was free
text the user had to type correctly by hand). No regression: without
row_id the widget still falls back to the latest row, exactly as
before.

Extract data_definition_options() (rows+fields for a definition) out
of routes/screens/screen_edit.py so controls_with_values.py can reuse
it server-side for the initial render; a small client-side handler
(bindJaugeDefinitionSelect) repopulates the same selects live when
the tracked object is changed without leaving the panel.

Verified live with Playwright: picking "jauge" then "Confiance" then
"value"/"name" in the panel renders an 80%-filled, green-leaning bar
labelled "Confiance" on /play — not the 20%/50% of the other rows.
2026-08-23 18:02:02 +02:00
williamandwilliam 3f4ebc4527 first commit
Build and deploy / build-and-push (push) Successful in 17s
Build and deploy / deploy (push) Successful in 10s
2026-08-21 16:23:49 +02:00