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>
This commit is contained in:
william
2026-08-28 09:05:02 +02:00
co-authored by Claude Sonnet 5
parent 7c237d6f1c
commit 8cffbeac68
21 changed files with 971 additions and 93 deletions
+80
View File
@@ -203,6 +203,37 @@ def test_overlay_box_default_text_color_survives_the_bulma_box_class(client, gam
assert "color:#e8eaf0" in box_tag
def test_visibility_dropdown_reflects_masque_even_when_visible_would_also_match(client, game):
"""Régression (la vraie cause derrière "je ne vois plus ma modale, ni
même en changeant Visibilité sur Visible") : le panneau de propriétés
détecte la valeur ACTUELLE d'un réglage "preset" (screens/widgets/
control_value.py) en cherchant la PREMIÈRE option de la liste dont les
critères correspondent au style stocké. L'option "Visible normalement"
de la Visibilité (visibility_control.py) ne vérifie QUE "visibility"
(jamais "display", par choix assumé) — un élément "Masqué" (qui ne pose
que "display:none", jamais "visibility") satisfaisait donc TOUJOURS,
trivialement, les critères de "Visible" en premier (testée avant
"Masqué" dans la liste) : le panneau affichait "Visible normalement"
sélectionné sur un élément EN RÉALITÉ masqué. Comme un <select> ne
déclenche un enregistrement que sur un changement RÉEL de valeur,
re-choisir l'option déjà affichée ne faisait RIEN : impossible de
rendre l'élément visible depuis le panneau. Corrigé en vérifiant les
options les plus SPÉCIFIQUES (le plus de propriétés non vides exigées)
en premier."""
from screens.widgets.control_value import _control_value
from screens.widgets.widget_meta import widget_meta
import screens
screen_id = _create_screen(client, game)
overlay_id = _add_element(client, game, screen_id, "superposition")
el = screens.get_element(game, overlay_id)
meta = widget_meta(el["widget"])
vis_control = next(c for c in meta["controls"] if c["key"] == "visibilite")
# "Masqué" par défaut à la création (default_style_for_widget.py) : le
# panneau doit détecter "masque", pas "visible".
assert _control_value(el, vis_control) == "masque"
def test_overlay_behaves_like_a_normal_container_in_the_editor(client, game):
"""Régression (deux retours utilisateur successifs) :
1. Un essai précédent forçait "display:flex" dans l'éditeur, quel que
@@ -284,6 +315,55 @@ def test_overlay_element_type_instance_has_no_visible_wrapper_box(client, game):
assert "z-index" not in wrapper_style
def test_overlay_element_type_instance_is_controllable_from_the_hosting_scene(client, game):
"""Régression : la logique posée sur LA SCÈNE qui accueille un
exemplaire de dialogue (ex. "Modifier un élément → Modale : Visibilité
= Rendre visible", ciblant l'exemplaire par son id SUR CETTE SCÈNE)
n'avait plus aucun effet une fois l'enveloppe "conteneur" entièrement
court-circuitée (un essai précédent) : son id disparaissait du DOM
(impossible à cibler), et même en le gardant, la superposition INTERNE
au modèle restait masquée indépendamment (double masquage — rendre
l'enveloppe visible n'aurait rien changé). Corrigé : l'enveloppe
GARDE son propre id/data-element-id (ciblable depuis la scène), et la
superposition interne au modèle ignore désormais son propre réglage
"Visibilité" une fois posée comme exemplaire — tout le masquage est
délégué à l'enveloppe (qui démarre elle-même masquée par défaut, voir
add_element.py)."""
import screens
resp = client.post(f"/game/{game}/element-types", data={"name": "Dialogue2"}, follow_redirects=False)
et = next(t for t in screens.list_element_types(game) if t["name"] == "Dialogue2")
overlay_id = _add_element(client, game, et["template_screen_id"], "superposition")
client.post(f"/game/{game}/elements/{overlay_id}/children/add", data={"widget": "texte"})
screen_id = _create_screen(client, game, "Scène2")
resp = client.post(f"/game/{game}/screens/{screen_id}/elements/add",
data={"widget": "__catalogue__", "element_type_id": et["id"]}, follow_redirects=False)
instance_id = int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1))
html = client.get(f"/game/{game}/play").data.decode()
idx = html.find(f'data-element-id="{instance_id}"')
assert idx != -1
wrapper_tag = html[html.rfind("<div", 0, idx):idx + 40]
assert "display:none" in wrapper_tag # masqué par défaut, sur l'EXEMPLAIRE
# La superposition interne au modèle, elle, ne doit PLUS porter son
# propre display:none une fois rendue comme exemplaire (sinon la
# rendre visible depuis la scène resterait sans effet).
idx_modal = html.find('class="modal is-active"', idx)
modal_tag = html[html.rfind("<div", 0, idx_modal):idx_modal + 250]
assert "display:none" not in modal_tag
trig = client.post(f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps({"node_type": "trigger", "trigger_event": "affichage"}),
content_type="application/json").get_json()
act = client.post(f"/game/{game}/screens/{screen_id}/flow/nodes/add",
data=json.dumps({
"node_type": "action", "action_type": "modifier_element",
"target_element_id": instance_id, "element_property": "visibilite", "element_value": "visible",
}), content_type="application/json").get_json()
assert act["target_element_id"] == instance_id
def test_overlay_wrapper_does_not_trap_its_own_z_index(client, game):
"""Régression : le cadre .playElement/.canvasElement partagé par TOUS
les widgets (voir filters/element_style_filter.py) posait quand même