Files
Forge-Engine/screens/widgets/control_value.py
T
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

96 lines
4.0 KiB
Python

import re
from ..clause_list_codec import decode_clauses
def _control_value(el, control):
"""Lit la valeur actuelle d'un réglage à partir de l'élément stocké
(content / attributs / style), avec repli sur sa valeur par défaut."""
ctype = control["type"]
style = el.get("style") or {}
if ctype == "clause_list":
attrs = el.get("attributes") or {}
clauses, combinateur = decode_clauses(
attrs, control["clauses_attr"], control["combinator_attr"],
legacy_prefix=control.get("legacy_prefix"),
)
return {"clauses": clauses, "combinateur": combinateur}
if ctype == "preset":
# Trié par nombre de propriétés NON VIDES exigées, décroissant : une
# option comme "Visible normalement" (visibility_control.py), dont
# les critères ({"visibility": ""}) ne portent QUE sur "visibility"
# (par choix assumé, pour ne jamais toucher "display" — voir son
# commentaire), matchait sinon TOUJOURS en premier dès que
# "visibility" est absent, MÊME quand "display:none" (posé par
# "Masqué", un critère plus spécifique à 1 propriété non vide contre
# 0 pour "Visible") est également présent -- le panneau affichait
# alors "Visible normalement" sélectionné sur un élément en réalité
# masqué, et comme un <select> ne redéclenche un enregistrement que
# sur un VRAI changement de valeur, il devenait impossible de le
# rendre visible depuis le panneau (choisir l'option déjà affichée
# ne fait rien). Les options plus spécifiques (plus de critères non
# vides) doivent donc toujours être vérifiées EN PREMIER.
options = sorted(
control["options"],
key=lambda o: -sum(1 for v in o[2].values() if v),
)
for key, _label, props in options:
if all((style.get(k) or "") == (v or "") for k, v in props.items()):
return key
return control.get("default")
if ctype == "toggle_token":
_kind, name = control["target"].split(":", 1)
current = style.get(name, "") or ""
return control["token"] in current.split()
if ctype == "scale":
transform = style.get("transform", "") or ""
m = re.search(r"scale\(([\d.]+)\)", transform)
return float(m.group(1)) if m else control.get("default", 1)
if ctype == "size":
_kind, name = control["target"].split(":", 1)
raw = style.get(name)
m = re.match(r"^([\d.]+)(px|%)$", str(raw or ""))
if m:
return {"value": float(m.group(1)), "unit": m.group(2)}
return {"value": control.get("default", 0), "unit": "%"}
if ctype == "shadow":
_kind, name = control["target"].split(":", 1)
raw = style.get(name)
m = re.match(
r"^(-?\d+)px (-?\d+)px (\d+)px (\d+)px rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)$",
str(raw or ""),
)
if m:
x, y, blur, spread, r, g, b, a = m.groups()
return {
"x": int(x), "y": int(y), "blur": int(blur), "spread": int(spread),
"color": "#{:02x}{:02x}{:02x}".format(int(r), int(g), int(b)), "opacity": float(a),
}
return control.get("default", {})
target = control["target"]
if target == "content":
return el.get("content") or control.get("default", "")
kind, name = target.split(":", 1)
store = el.get("attributes") if kind == "attr" else style
store = store or {}
if ctype == "checkbox":
if name not in store:
return bool(control.get("default", False))
return store[name] not in (None, "", control.get("false_value"))
if ctype == "slider":
raw = store.get(name)
if raw is None:
return control.get("default", 0)
try:
return float(str(raw).replace(control.get("unit", ""), "").strip() or 0)
except ValueError:
return control.get("default", 0)
return store.get(name, control.get("default", ""))