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>
105 lines
5.2 KiB
Python
105 lines
5.2 KiB
Python
"""Un nœud Condition du graphe de logique peut désormais tester une
|
|
VARIABLE GLOBALE, pas seulement un champ d'objet (voir
|
|
ensure_flow_schema.py : cond_source/cond_variable/cond_variable_chemin).
|
|
Évalué côté CLIENT (templates/play.html, evaluateConditionClause) contre
|
|
gameData.variables — exposé par full_game_payload.py, vérifié ici via
|
|
/runtime-payload. La logique JS elle-même (résolution de chemin, lecture
|
|
de gameData.variables, repli sur le comportement historique) a été vérifiée
|
|
manuellement avec Node.js (fonctions extraites de play.html) : variable
|
|
scalaire, objet avec chemin chaîné, tableau par index, variable
|
|
introuvable (ne matche rien plutôt que planter), booléen "Oui", et clause
|
|
sans "source" (comportement legacy, champ d'objet) — les 7 scénarios
|
|
passent."""
|
|
import json
|
|
import re
|
|
|
|
|
|
def _create_screen(client, slug, name="Accueil"):
|
|
resp = client.post(f"/game/{slug}/screens/new", data={"name": name}, follow_redirects=False)
|
|
return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1))
|
|
|
|
|
|
def test_condition_node_stores_variable_source(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "condition", "cond_source": "variable",
|
|
"cond_variable": "score", "cond_variable_chemin": "",
|
|
"cond_operator": "superieur_egal", "cond_value": "3",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node = resp.get_json()
|
|
assert node["cond_source"] == "variable"
|
|
assert node["cond_variable"] == "score"
|
|
assert node["cond_operator"] == "superieur_egal"
|
|
# Aucun champ d'objet renseigné pour une clause "variable".
|
|
assert node["cond_definition_id"] is None
|
|
assert node["cond_field"] is None
|
|
|
|
|
|
def test_condition_node_with_combined_variable_and_object_clauses(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
resp = client.post(f"/game/{game}/objects/new", data={
|
|
"object_name": "Niveau", "field_name[]": ["numero"], "field_type[]": ["nombre_entier"],
|
|
"field_relation[]": [""], "field_required[]": ["0"], "field_min[]": [""], "field_max[]": [""],
|
|
}, follow_redirects=False)
|
|
def_id = int(resp.headers["Location"].rstrip("/").split("/")[-1])
|
|
screen_id = _create_screen(client, game)
|
|
|
|
clauses = [
|
|
{"source": "variable", "variable": "score", "operator": "superieur_egal", "value": "3"},
|
|
{"source": "objet", "definition_id": def_id, "row_id": None, "field": "numero",
|
|
"field_type": "nombre_entier", "operator": "egal", "value": "1"},
|
|
]
|
|
resp = client.post(
|
|
f"/game/{game}/screens/{screen_id}/flow/nodes/add",
|
|
data=json.dumps({
|
|
"node_type": "condition", "cond_clauses": json.dumps(clauses), "cond_combinator": "ou",
|
|
}),
|
|
content_type="application/json",
|
|
)
|
|
assert resp.status_code == 200
|
|
node = resp.get_json()
|
|
stored = json.loads(node["cond_clauses"])
|
|
assert stored[0]["source"] == "variable"
|
|
assert stored[1]["source"] == "objet"
|
|
assert node["cond_combinator"] == "ou"
|
|
|
|
|
|
def test_runtime_payload_exposes_global_variables_for_condition_evaluation(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
client.post(f"/game/{game}/variables/new", data={"name": "perso", "type": "objet",
|
|
"default_value": json.dumps({"nom": "Alice"})})
|
|
payload = client.get(f"/game/{game}/runtime-payload").get_json()
|
|
assert payload["variables"]["score"] == {"value": "5", "type": "nombre_entier"}
|
|
assert payload["variables"]["perso"]["type"] == "objet"
|
|
assert json.loads(payload["variables"]["perso"]["value"]) == {"nom": "Alice"}
|
|
|
|
|
|
def test_screen_edit_panel_exposes_variable_source_condition_ui(client, game):
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert 'id="nf-cond-source"' in html
|
|
assert 'id="nf-cond-variable-fields"' in html
|
|
assert "onFlowConditionSourceChange" in html
|
|
assert "onFlowClauseSourceChange" in html
|
|
|
|
|
|
def test_condition_builder_still_available_without_any_object_defined(client, game):
|
|
"""Régression : le panneau de condition était entièrement masqué quand
|
|
le jeu n'avait AUCUN objet ("Aucun objet défini..."), rendant
|
|
impossible de créer une condition sur une variable dans ce cas. Seule
|
|
la partie "Champ d'un objet" doit rester masquée ; "Variable globale"
|
|
doit rester utilisable."""
|
|
client.post(f"/game/{game}/variables/new", data={"name": "score", "type": "nombre_entier", "default_value": "5"})
|
|
screen_id = _create_screen(client, game)
|
|
html = client.get(f"/game/{game}/screens/{screen_id}/edit").get_data(as_text=True)
|
|
assert 'id="nf-cond-source"' in html
|
|
assert 'id="nf-cond-variable-fields"' in html
|