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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3c6d07cb3a
commit
c04bc0b926
@@ -52,6 +52,12 @@ from .rows.update_row_field import update_row_field
|
|||||||
from .rows.delete_row import delete_row
|
from .rows.delete_row import delete_row
|
||||||
from .rows.rows_referencing import rows_referencing
|
from .rows.rows_referencing import rows_referencing
|
||||||
|
|
||||||
|
from .global_vars.list_global_variables import list_global_variables
|
||||||
|
from .global_vars.get_global_variable import get_global_variable
|
||||||
|
from .global_vars.create_global_variable import create_global_variable
|
||||||
|
from .global_vars.update_global_variable_value import update_global_variable_value
|
||||||
|
from .global_vars.delete_global_variable import delete_global_variable
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"PROJECTS_DIR", "FIELD_TYPES",
|
"PROJECTS_DIR", "FIELD_TYPES",
|
||||||
"slugify", "table_name_for", "game_dir", "db_path", "connect",
|
"slugify", "table_name_for", "game_dir", "db_path", "connect",
|
||||||
@@ -61,4 +67,6 @@ __all__ = [
|
|||||||
"delete_definition",
|
"delete_definition",
|
||||||
"list_rows", "relation_options", "insert_row", "get_row", "update_row",
|
"list_rows", "relation_options", "insert_row", "get_row", "update_row",
|
||||||
"update_row_field", "delete_row", "rows_referencing",
|
"update_row_field", "delete_row", "rows_referencing",
|
||||||
|
"list_global_variables", "get_global_variable", "create_global_variable",
|
||||||
|
"update_global_variable_value", "delete_global_variable",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
from .ensure_global_vars_schema import ensure_global_vars_schema
|
||||||
|
|
||||||
|
|
||||||
|
def create_global_variable(slug, name, var_type="texte", default_value=""):
|
||||||
|
"""Crée une variable globale si son nom n'existe pas déjà (les noms sont
|
||||||
|
uniques — voir ensure_global_vars_schema.py) ; si elle existe déjà, ne
|
||||||
|
touche à rien et renvoie simplement son id existant, pour que "créer"
|
||||||
|
depuis le sélecteur de condition de visibilité (voir screen_edit.html)
|
||||||
|
reste sans risque même en cas de double clic/soumission."""
|
||||||
|
name = (name or "").strip()
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
ensure_global_vars_schema(slug)
|
||||||
|
conn = connect(slug)
|
||||||
|
existing = conn.execute("SELECT id FROM _global_variables WHERE name = ?", (name,)).fetchone()
|
||||||
|
if existing:
|
||||||
|
conn.close()
|
||||||
|
return existing["id"]
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO _global_variables (name, type, value) VALUES (?, ?, ?)",
|
||||||
|
(name, var_type, default_value),
|
||||||
|
)
|
||||||
|
new_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return new_id
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
from .ensure_global_vars_schema import ensure_global_vars_schema
|
||||||
|
|
||||||
|
|
||||||
|
def delete_global_variable(slug, name):
|
||||||
|
ensure_global_vars_schema(slug)
|
||||||
|
conn = connect(slug)
|
||||||
|
conn.execute("DELETE FROM _global_variables WHERE name = ?", (name,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_global_vars_schema(slug):
|
||||||
|
"""Migration légère (même principe que ensure_field_bounds_schema.py) :
|
||||||
|
crée _global_variables si absente — aucune migration manuelle nécessaire
|
||||||
|
pour les jeux créés avant cette fonctionnalité, la prochaine lecture
|
||||||
|
suffit. Une variable globale vit pour TOUT le jeu (pas par écran, pas
|
||||||
|
par ligne) : "nom" est donc UNIQUE, une seule valeur actuelle par nom."""
|
||||||
|
conn = connect(slug)
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS _global_variables (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL UNIQUE,
|
||||||
|
type TEXT NOT NULL DEFAULT 'texte',
|
||||||
|
value TEXT,
|
||||||
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
from .ensure_global_vars_schema import ensure_global_vars_schema
|
||||||
|
|
||||||
|
|
||||||
|
def get_global_variable(slug, name):
|
||||||
|
"""Par NOM (unique), pas par id — c'est comme ça qu'une variable est
|
||||||
|
référencée partout ailleurs (condition de visibilité, action "Modifier
|
||||||
|
une variable"), jamais par un id technique que l'utilisateur ne voit
|
||||||
|
pas."""
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
ensure_global_vars_schema(slug)
|
||||||
|
conn = connect(slug)
|
||||||
|
row = conn.execute("SELECT * FROM _global_variables WHERE name = ?", (name,)).fetchone()
|
||||||
|
conn.close()
|
||||||
|
return dict(row) if row else None
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
from .ensure_global_vars_schema import ensure_global_vars_schema
|
||||||
|
|
||||||
|
|
||||||
|
def list_global_variables(slug):
|
||||||
|
ensure_global_vars_schema(slug)
|
||||||
|
conn = connect(slug)
|
||||||
|
rows = conn.execute("SELECT * FROM _global_variables ORDER BY name").fetchall()
|
||||||
|
conn.close()
|
||||||
|
return [dict(r) for r in rows]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from ..connection import connect
|
||||||
|
from .ensure_global_vars_schema import ensure_global_vars_schema
|
||||||
|
from .get_global_variable import get_global_variable
|
||||||
|
|
||||||
|
|
||||||
|
def update_global_variable_value(slug, name, new_value):
|
||||||
|
"""Écrit une nouvelle valeur BRUTE (déjà calculée par l'appelant) sur la
|
||||||
|
variable — utilisé aussi bien par l'action "Modifier une variable" (voir
|
||||||
|
screens/global_vars/apply_variable_action.py) que par une éventuelle
|
||||||
|
modification directe (page de gestion des variables)."""
|
||||||
|
if not get_global_variable(slug, name):
|
||||||
|
return False
|
||||||
|
ensure_global_vars_schema(slug)
|
||||||
|
conn = connect(slug)
|
||||||
|
conn.execute("UPDATE _global_variables SET value = ? WHERE name = ?", (str(new_value), name))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
from . import games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations
|
from . import games, objects, screens, elements, legacy_actions, flow, element_types, uploads, play, animations, global_vars
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ from . import (
|
|||||||
flow_edge_add,
|
flow_edge_add,
|
||||||
flow_edge_delete,
|
flow_edge_delete,
|
||||||
flow_node_run_data,
|
flow_node_run_data,
|
||||||
|
flow_node_run_variable,
|
||||||
runtime_payload,
|
runtime_payload,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from flask import jsonify
|
||||||
|
|
||||||
|
import screens
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/game/<slug>/flow/nodes/<int:node_id>/run-variable", methods=["POST"])
|
||||||
|
def flow_node_run_variable(slug, node_id):
|
||||||
|
"""Exécuté côté serveur au moment du clic, en mode jouable, pour un nœud
|
||||||
|
Action de type "Modifier une variable globale" — même principe que
|
||||||
|
flow_node_run_data.py, pour une variable plutôt qu'un champ d'objet."""
|
||||||
|
node = screens.get_flow_node(slug, node_id)
|
||||||
|
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_variable":
|
||||||
|
return jsonify({"ok": False}), 400
|
||||||
|
ok = screens.apply_variable_action(slug, node)
|
||||||
|
return jsonify({"ok": ok})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from .create_global_var import create_global_var
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from flask import jsonify, request
|
||||||
|
|
||||||
|
import db
|
||||||
|
|
||||||
|
from core.flask_app import app
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/game/<slug>/global-vars", methods=["POST"])
|
||||||
|
def create_global_var(slug):
|
||||||
|
"""Création rapide d'une variable globale depuis le sélecteur "Variable"
|
||||||
|
de la Condition de visibilité (voir screen_edit.html, openVisCondVariablePrompt) —
|
||||||
|
évite d'avoir à quitter le panneau de propriétés pour en créer une."""
|
||||||
|
name = (request.form.get("name") or "").strip()
|
||||||
|
var_type = request.form.get("type") or "texte"
|
||||||
|
default_value = request.form.get("default_value") or ""
|
||||||
|
if not name:
|
||||||
|
return jsonify({"ok": False, "error": "Nom manquant"}), 400
|
||||||
|
var_id = db.create_global_variable(slug, name, var_type, default_value)
|
||||||
|
return jsonify({"ok": True, "id": var_id, "name": name})
|
||||||
@@ -38,6 +38,7 @@ def screen_edit(slug, screen_id):
|
|||||||
elements_labels = {el["id"]: screens.element_label(el, screens.WIDGETS) for el in elements}
|
elements_labels = {el["id"]: screens.element_label(el, screens.WIDGETS) for el in elements}
|
||||||
screens_map = {s["id"]: s["name"] for s in all_screens}
|
screens_map = {s["id"]: s["name"] for s in all_screens}
|
||||||
animation_clips = screens.list_animation_clips(slug, screen_id)
|
animation_clips = screens.list_animation_clips(slug, screen_id)
|
||||||
|
global_variables = db.list_global_variables(slug)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"screen_edit.html",
|
"screen_edit.html",
|
||||||
@@ -74,4 +75,5 @@ def screen_edit(slug, screen_id):
|
|||||||
animate_css_catalog=screens.ANIMATE_CSS_CATALOG,
|
animate_css_catalog=screens.ANIMATE_CSS_CATALOG,
|
||||||
animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
|
animate_css_labels_json=json.dumps(screens.ANIMATE_CSS_LABELS),
|
||||||
fontawesome_icons=screens.FONTAWESOME_ICONS,
|
fontawesome_icons=screens.FONTAWESOME_ICONS,
|
||||||
|
global_variables=global_variables,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ from .flow.add_flow_edge import add_flow_edge
|
|||||||
from .flow.delete_flow_edge import delete_flow_edge
|
from .flow.delete_flow_edge import delete_flow_edge
|
||||||
|
|
||||||
from .data_actions.apply_data_action import apply_data_action
|
from .data_actions.apply_data_action import apply_data_action
|
||||||
|
from .data_actions.apply_variable_action import apply_variable_action
|
||||||
|
|
||||||
from .animations.ensure_animation_schema import ensure_animation_schema
|
from .animations.ensure_animation_schema import ensure_animation_schema
|
||||||
from .animations.list_animation_clips import list_animation_clips
|
from .animations.list_animation_clips import list_animation_clips
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import db
|
||||||
|
|
||||||
|
|
||||||
|
def apply_variable_action(slug, action):
|
||||||
|
"""Exécute au moment du clic (mode jouable) une action
|
||||||
|
"modifier_variable" — même logique/vocabulaire d'opérations que
|
||||||
|
apply_data_action.py (DATA_OPERATIONS), appliqué à une variable globale
|
||||||
|
(une seule valeur pour tout le jeu) plutôt qu'au champ d'une ligne
|
||||||
|
d'objet."""
|
||||||
|
var_name = action.get("target_variable")
|
||||||
|
operation = action.get("data_operation")
|
||||||
|
raw_value = action.get("data_value")
|
||||||
|
if not (var_name and operation):
|
||||||
|
return False
|
||||||
|
variable = db.get_global_variable(slug, var_name)
|
||||||
|
if not variable:
|
||||||
|
return False
|
||||||
|
var_type = variable["type"]
|
||||||
|
current = variable.get("value")
|
||||||
|
if var_type in ("nombre_entier", "nombre_decimal"):
|
||||||
|
try:
|
||||||
|
current = float(current) if current not in (None, "") else 0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
current = 0
|
||||||
|
elif var_type == "booleen":
|
||||||
|
current = 1 if str(current).strip().lower() in ("1", "true", "vrai") else 0
|
||||||
|
|
||||||
|
if operation == "definir_texte":
|
||||||
|
new_value = raw_value or ""
|
||||||
|
elif operation == "definir_nombre":
|
||||||
|
try:
|
||||||
|
new_value = float(raw_value) if var_type == "nombre_decimal" else int(float(raw_value))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
new_value = 0
|
||||||
|
elif operation == "definir_bool_vrai":
|
||||||
|
new_value = 1
|
||||||
|
elif operation == "definir_bool_faux":
|
||||||
|
new_value = 0
|
||||||
|
elif operation == "basculer_bool":
|
||||||
|
new_value = 0 if current else 1
|
||||||
|
elif operation in ("incrementer", "decrementer"):
|
||||||
|
try:
|
||||||
|
amount = float(raw_value) if raw_value not in (None, "") else 0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
amount = 0
|
||||||
|
if operation == "decrementer":
|
||||||
|
amount = -amount
|
||||||
|
result = (current or 0) + amount
|
||||||
|
new_value = result if var_type == "nombre_decimal" else int(result)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return db.update_global_variable_value(slug, var_name, new_value)
|
||||||
@@ -5,14 +5,20 @@ import db
|
|||||||
from ..rendering.render_element_html import render_element_html
|
from ..rendering.render_element_html import render_element_html
|
||||||
|
|
||||||
|
|
||||||
def list_elements(slug, screen_id):
|
def list_elements(slug, screen_id, enforce_visibility=False):
|
||||||
"""Renvoie TOUS les éléments de l'écran, à plat (parent_id indique
|
"""Renvoie TOUS les éléments de l'écran, à plat (parent_id indique
|
||||||
l'imbrication) — pratique pour construire l'arbre côté appelant. Seuls
|
l'imbrication) — pratique pour construire l'arbre côté appelant. Seuls
|
||||||
les éléments de premier niveau (parent_id NULL) reçoivent un
|
les éléments de premier niveau (parent_id NULL) reçoivent un
|
||||||
"rendered_html" : celui d'un conteneur/répéteur/groupe de champs inclut
|
"rendered_html" : celui d'un conteneur/répéteur/groupe de champs inclut
|
||||||
déjà, en le générant récursivement, le HTML de tout ce qui est posé à
|
déjà, en le générant récursivement, le HTML de tout ce qui est posé à
|
||||||
l'intérieur — les enfants n'ont donc pas besoin d'être dessinés une
|
l'intérieur — les enfants n'ont donc pas besoin d'être dessinés une
|
||||||
deuxième fois séparément."""
|
deuxième fois séparément.
|
||||||
|
|
||||||
|
enforce_visibility : True uniquement pour le mode jouable (voir
|
||||||
|
full_game_payload.py) — applique la "condition de visibilité" de chaque
|
||||||
|
élément (voir visibility_condition.py). Toujours False pour l'éditeur,
|
||||||
|
qui doit continuer à montrer/sélectionner un élément même si sa
|
||||||
|
condition serait actuellement fausse."""
|
||||||
conn = db.connect(slug)
|
conn = db.connect(slug)
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT * FROM _screen_elements WHERE screen_id = ? ORDER BY z_index, id", (screen_id,)
|
"SELECT * FROM _screen_elements WHERE screen_id = ? ORDER BY z_index, id", (screen_id,)
|
||||||
@@ -28,7 +34,8 @@ def list_elements(slug, screen_id):
|
|||||||
for d in result:
|
for d in result:
|
||||||
if d.get("parent_id"):
|
if d.get("parent_id"):
|
||||||
children_map.setdefault(d["parent_id"], []).append(d)
|
children_map.setdefault(d["parent_id"], []).append(d)
|
||||||
|
play_ctx = {"_forge_play_mode": True} if enforce_visibility else None
|
||||||
for d in result:
|
for d in result:
|
||||||
if not d.get("parent_id"):
|
if not d.get("parent_id"):
|
||||||
d["rendered_html"] = render_element_html(d, slug, children_map)
|
d["rendered_html"] = render_element_html(d, slug, children_map, play_ctx)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -53,4 +53,5 @@ FLOW_NODE_FIELDS = {
|
|||||||
"cond_clauses", "cond_combinator",
|
"cond_clauses", "cond_combinator",
|
||||||
"action_type", "target_screen_id", "target_element_id", "element_property", "element_value",
|
"action_type", "target_screen_id", "target_element_id", "element_property", "element_value",
|
||||||
"target_definition_id", "target_row_id", "target_field", "data_operation", "data_value",
|
"target_definition_id", "target_row_id", "target_field", "data_operation", "data_value",
|
||||||
|
"target_variable",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,5 +51,10 @@ def ensure_flow_schema(slug):
|
|||||||
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_clauses TEXT")
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_clauses TEXT")
|
||||||
if "cond_combinator" not in cols:
|
if "cond_combinator" not in cols:
|
||||||
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_combinator TEXT NOT NULL DEFAULT 'et'")
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN cond_combinator TEXT NOT NULL DEFAULT 'et'")
|
||||||
|
if "target_variable" not in cols:
|
||||||
|
# Action "Modifier une variable" (voir apply_variable_action.py) —
|
||||||
|
# cible une variable globale par NOM plutôt qu'un champ d'objet ;
|
||||||
|
# réutilise data_operation/data_value, déjà là pour "modifier_donnee".
|
||||||
|
conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_variable TEXT")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ ACTION_TYPE_LABELS = {
|
|||||||
"aller_a": "Aller à un écran précis",
|
"aller_a": "Aller à un écran précis",
|
||||||
"modifier_element": "Modifier un élément",
|
"modifier_element": "Modifier un élément",
|
||||||
"modifier_donnee": "Modifier une donnée d'un objet",
|
"modifier_donnee": "Modifier une donnée d'un objet",
|
||||||
|
"modifier_variable": "Modifier une variable globale",
|
||||||
"ouvrir_ligne": "Ouvrir la ligne de Répéteur cliquée",
|
"ouvrir_ligne": "Ouvrir la ligne de Répéteur cliquée",
|
||||||
"activer_onglet": "Afficher cet élément, masquer tous ses frères",
|
"activer_onglet": "Afficher cet élément, masquer tous ses frères",
|
||||||
"attendre": "Attendre quelques secondes avant de continuer",
|
"attendre": "Attendre quelques secondes avant de continuer",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def full_game_payload(slug):
|
|||||||
flows = {}
|
flows = {}
|
||||||
animations = {}
|
animations = {}
|
||||||
for s in screens_:
|
for s in screens_:
|
||||||
elements = list_elements(slug, s["id"])
|
elements = list_elements(slug, s["id"], enforce_visibility=True)
|
||||||
payload_screens.append({**s, "elements": elements})
|
payload_screens.append({**s, "elements": elements})
|
||||||
flows[str(s["id"])] = {
|
flows[str(s["id"])] = {
|
||||||
"nodes": list_flow_nodes(slug, s["id"]),
|
"nodes": list_flow_nodes(slug, s["id"]),
|
||||||
|
|||||||
@@ -5,5 +5,10 @@ def _apply_ctx(text, ctx):
|
|||||||
if not ctx or not text:
|
if not ctx or not text:
|
||||||
return text
|
return text
|
||||||
for k, v in ctx.items():
|
for k, v in ctx.items():
|
||||||
|
# "_forge_play_mode" (voir render_element_html.py) est un indicateur
|
||||||
|
# interne posé dans ce même ctx, pas une valeur de champ {{...}} —
|
||||||
|
# à ignorer ici.
|
||||||
|
if k == "_forge_play_mode":
|
||||||
|
continue
|
||||||
text = text.replace("{{" + k + "}}", v)
|
text = text.replace("{{" + k + "}}", v)
|
||||||
return text
|
return text
|
||||||
|
|||||||
@@ -51,7 +51,15 @@ def _compare(actual, operator, expected, field_type):
|
|||||||
"""Équivalent Python de compareValues() dans templates/play.html — les
|
"""Équivalent Python de compareValues() dans templates/play.html — les
|
||||||
deux doivent rester alignées si on ajoute un opérateur."""
|
deux doivent rester alignées si on ajoute un opérateur."""
|
||||||
if field_type == "booleen":
|
if field_type == "booleen":
|
||||||
a = 1 if actual else 0
|
# "actual" est un int SQLite (0/1) pour un champ d'objet, mais une
|
||||||
|
# chaîne pour une variable globale (voir db/global_vars/, toujours
|
||||||
|
# stockée en TEXT) — "0" est une chaîne non vide donc VRAIE en
|
||||||
|
# Python, d'où ce traitement explicite plutôt qu'un simple bool(...).
|
||||||
|
if isinstance(actual, str):
|
||||||
|
truthy = actual.strip().lower() not in ("", "0", "false", "faux", "none")
|
||||||
|
else:
|
||||||
|
truthy = bool(actual)
|
||||||
|
a = 1 if truthy else 0
|
||||||
e = 1 if str(expected).strip().lower() in ("1", "true", "vrai") else 0
|
e = 1 if str(expected).strip().lower() in ("1", "true", "vrai") else 0
|
||||||
return a != e if operator == "different" else a == e
|
return a != e if operator == "different" else a == e
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from .render_overlay import _render_overlay
|
|||||||
from .render_onglets import _render_onglets
|
from .render_onglets import _render_onglets
|
||||||
from .render_icone import _render_icone
|
from .render_icone import _render_icone
|
||||||
from .resolve_bound_row import _resolve_bound_row_ctx
|
from .resolve_bound_row import _resolve_bound_row_ctx
|
||||||
|
from .visibility_condition import is_element_visible
|
||||||
|
|
||||||
|
|
||||||
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
|
def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_direction=None):
|
||||||
@@ -31,7 +32,28 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
|||||||
pose physiquement cet élément, s'il y en a un — elle influence la largeur
|
pose physiquement cet élément, s'il y en a un — elle influence la largeur
|
||||||
par défaut de cet élément (voir _style_string)."""
|
par défaut de cet élément (voir _style_string)."""
|
||||||
meta = widget_meta(el.get("widget"))
|
meta = widget_meta(el.get("widget"))
|
||||||
bound_ctx = _resolve_bound_row_ctx(slug, el.get("attributes") or {})
|
attrs_raw = el.get("attributes") or {}
|
||||||
|
# La condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) n'est
|
||||||
|
# appliquée qu'en mode JOUABLE (ctx["_forge_play_mode"], posé par
|
||||||
|
# full_game_payload.py via list_elements(..., enforce_visibility=True))
|
||||||
|
# — jamais dans l'éditeur, où l'élément doit rester visible/sélectionnable
|
||||||
|
# même si la condition serait actuellement fausse, sans quoi il
|
||||||
|
# deviendrait impossible de le retrouver pour le régler. Un commentaire
|
||||||
|
# marqueur ("visibilityGated") reste posé sur le HTML même quand la
|
||||||
|
# condition est vraie, pour que refreshRuntimeData() (play.html) sache
|
||||||
|
# qu'il faut réévaluer cet élément après toute action "Modifier une
|
||||||
|
# donnée"/"Modifier une variable" — pas seulement Jauge/Répéteur, qui ont
|
||||||
|
# déjà chacun leur propre marqueur ("jaugeBar"/"repeaterItem") couvrant
|
||||||
|
# ce cas d'office.
|
||||||
|
has_visibility_condition = attrs_raw.get("_visibilite_cond_mode") in ("variable", "objet")
|
||||||
|
play_mode = bool((ctx or {}).get("_forge_play_mode"))
|
||||||
|
if play_mode and has_visibility_condition and not is_element_visible(slug, attrs_raw):
|
||||||
|
return f'<div class="visibilityGated" data-element-id="{el["id"]}" style="display:none;"></div>'
|
||||||
|
|
||||||
|
def _mark(html_str):
|
||||||
|
return html_str + "<!--visibilityGated-->" if has_visibility_condition else html_str
|
||||||
|
|
||||||
|
bound_ctx = _resolve_bound_row_ctx(slug, attrs_raw)
|
||||||
if bound_ctx:
|
if bound_ctx:
|
||||||
ctx = {**(ctx or {}), **bound_ctx}
|
ctx = {**(ctx or {}), **bound_ctx}
|
||||||
special = meta.get("special_render")
|
special = meta.get("special_render")
|
||||||
@@ -72,7 +94,7 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
|||||||
attrs["data-hover-text"] = _apply_ctx(hover_text, ctx)
|
attrs["data-hover-text"] = _apply_ctx(hover_text, ctx)
|
||||||
|
|
||||||
if meta.get("self_closing"):
|
if meta.get("self_closing"):
|
||||||
return f"<{tag} {_attr_string(attrs, style)}>"
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>")
|
||||||
|
|
||||||
content = _apply_ctx(el.get("content") or "", ctx)
|
content = _apply_ctx(el.get("content") or "", ctx)
|
||||||
child_html = _render_children(el, slug, children_map, ctx)
|
child_html = _render_children(el, slug, children_map, ctx)
|
||||||
@@ -80,7 +102,7 @@ def render_element_html(el, slug=None, children_map=None, ctx=None, parent_flex_
|
|||||||
if tag in ("ul", "ol"):
|
if tag in ("ul", "ol"):
|
||||||
items = [line.strip() for line in content.split("\n") if line.strip()]
|
items = [line.strip() for line in content.split("\n") if line.strip()]
|
||||||
inner = "".join(f"<li>{html_lib.escape(item)}</li>" for item in items) + child_html
|
inner = "".join(f"<li>{html_lib.escape(item)}</li>" for item in items) + child_html
|
||||||
return f"<{tag} {_attr_string(attrs, style)}>{inner}</{tag}>"
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>{inner}</{tag}>")
|
||||||
|
|
||||||
safe_content = html_lib.escape(content).replace("\n", "<br>") if content else ""
|
safe_content = html_lib.escape(content).replace("\n", "<br>") if content else ""
|
||||||
return f"<{tag} {_attr_string(attrs, style)}>{safe_content}{child_html}</{tag}>"
|
return _mark(f"<{tag} {_attr_string(attrs, style)}>{safe_content}{child_html}</{tag}>")
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import db
|
||||||
|
|
||||||
|
from .filter_repeater_rows import _resolve_filter_value, _compare, _field_column
|
||||||
|
|
||||||
|
|
||||||
|
def is_element_visible(slug, attrs):
|
||||||
|
"""Condition de visibilité (panneau de propriétés, tout widget — voir
|
||||||
|
VISIBILITY_CONDITION_CONTROLS) : masque un élément selon soit une
|
||||||
|
VARIABLE GLOBALE (une seule valeur pour tout le jeu, voir
|
||||||
|
db/global_vars/), soit le champ d'un objet de données (convention "1
|
||||||
|
seule ligne = état de partie", déjà utilisée par la Jauge/Donnée liée).
|
||||||
|
Absente de réglage (_visibilite_cond_mode vide) => toujours visible,
|
||||||
|
comportement inchangé pour tous les éléments déjà créés. Réutilise
|
||||||
|
_compare/_resolve_filter_value/_field_column de filter_repeater_rows.py
|
||||||
|
pour rester cohérent avec les mêmes opérateurs et la même syntaxe
|
||||||
|
"{{Objet.champ}}" pour une valeur de comparaison dynamique."""
|
||||||
|
mode = attrs.get("_visibilite_cond_mode")
|
||||||
|
if mode not in ("variable", "objet"):
|
||||||
|
return True
|
||||||
|
operator = attrs.get("_visibilite_cond_operateur") or "egal"
|
||||||
|
expected = _resolve_filter_value(slug, attrs.get("_visibilite_cond_valeur") or "")
|
||||||
|
|
||||||
|
if mode == "variable":
|
||||||
|
var_name = attrs.get("_visibilite_cond_variable")
|
||||||
|
if not var_name:
|
||||||
|
return True
|
||||||
|
variable = db.get_global_variable(slug, var_name)
|
||||||
|
if not variable:
|
||||||
|
return True # variable supprimée depuis : ne bloque jamais l'affichage
|
||||||
|
return _compare(variable.get("value"), operator, expected, variable.get("type") or "texte")
|
||||||
|
|
||||||
|
definition_id = attrs.get("_visibilite_cond_definition_id")
|
||||||
|
field_name = attrs.get("_visibilite_cond_champ")
|
||||||
|
if not definition_id or not field_name:
|
||||||
|
return True
|
||||||
|
definition = db.get_definition(slug, int(definition_id))
|
||||||
|
if not definition:
|
||||||
|
return True
|
||||||
|
rows = db.list_rows(slug, definition)
|
||||||
|
if not rows:
|
||||||
|
return True # objet sans donnée pour l'instant : ne bloque pas
|
||||||
|
row = rows[0] # convention "état de partie" : une seule ligne, voir data_definition_options.py
|
||||||
|
field_type = next((f["type"] for f in definition["fields"] if f["name"] == field_name), "texte")
|
||||||
|
return _compare(row.get(_field_column(definition, field_name)), operator, expected, field_type)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from ..controls.c_scale import c_scale
|
from ..controls.c_scale import c_scale
|
||||||
from .visibility_control import VISIBILITY_CONTROL
|
from .visibility_control import VISIBILITY_CONTROL
|
||||||
|
from .visibility_condition_controls import VISIBILITY_CONDITION_CONTROLS
|
||||||
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
|
from .size_override_controls import SIZE_OVERRIDE_CONTROLS
|
||||||
from .spacing_controls import SPACING_CONTROLS
|
from .spacing_controls import SPACING_CONTROLS
|
||||||
from .border_controls import BORDER_CONTROLS
|
from .border_controls import BORDER_CONTROLS
|
||||||
@@ -11,4 +12,4 @@ from .border_controls import BORDER_CONTROLS
|
|||||||
# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui
|
# et bindHoverTexts() dans play.html) reste fonctionnel pour les éléments qui
|
||||||
# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît
|
# en avaient déjà un, seul le moyen de le RÉGLER depuis ce panneau disparaît
|
||||||
# pour l'instant.
|
# pour l'instant.
|
||||||
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS
|
UNIVERSAL_CONTROLS = [VISIBILITY_CONTROL, c_scale()] + SIZE_OVERRIDE_CONTROLS + SPACING_CONTROLS + BORDER_CONTROLS + VISIBILITY_CONDITION_CONTROLS
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from ..controls.c_text import c_text
|
||||||
|
from ..controls.c_select import c_select
|
||||||
|
from ...flow.constants import CONDITION_OPERATORS
|
||||||
|
|
||||||
|
_GROUP = "Condition de visibilité"
|
||||||
|
|
||||||
|
# Disponible sur TOUT widget (voir UNIVERSAL_CONTROLS) — masque l'élément en
|
||||||
|
# mode jouable (jamais dans l'éditeur, voir render_element_html.py) selon
|
||||||
|
# soit une variable globale (db/global_vars/), soit le champ d'un objet de
|
||||||
|
# données (convention "1 seule ligne = état de partie", déjà utilisée par la
|
||||||
|
# Jauge/Donnée liée). "visibilite_cond_valeur" se termine par "_valeur" :
|
||||||
|
# récupère automatiquement le sélecteur "Valeur fixe / Donnée d'un autre
|
||||||
|
# objet" déjà construit pour les filtres du Répéteur (voir screen_edit.html).
|
||||||
|
VISIBILITY_CONDITION_CONTROLS = [
|
||||||
|
c_select("visibilite_cond_mode", "Afficher cet élément seulement si...", "attr:_visibilite_cond_mode",
|
||||||
|
[("", "Toujours (pas de condition)"), ("variable", "Une variable globale..."), ("objet", "Un champ d'un objet...")],
|
||||||
|
default="", group=_GROUP),
|
||||||
|
c_select("visibilite_cond_variable", "Variable", "attr:_visibilite_cond_variable",
|
||||||
|
[("", "(sélectionner une variable)")], default="", group=_GROUP),
|
||||||
|
c_select("visibilite_cond_definition_id", "Objet", "attr:_visibilite_cond_definition_id",
|
||||||
|
[("", "(sélectionner un objet)")], default="", group=_GROUP),
|
||||||
|
c_select("visibilite_cond_champ", "Champ", "attr:_visibilite_cond_champ",
|
||||||
|
[("", "(sélectionner un objet d'abord)")], default="", group=_GROUP),
|
||||||
|
c_select("visibilite_cond_operateur", "...est", "attr:_visibilite_cond_operateur",
|
||||||
|
CONDITION_OPERATORS, default="egal", group=_GROUP),
|
||||||
|
c_text("visibilite_cond_valeur", "...cette valeur", "attr:_visibilite_cond_valeur",
|
||||||
|
placeholder="ex. 1", default="", group=_GROUP),
|
||||||
|
]
|
||||||
@@ -36,12 +36,21 @@ def controls_with_values(slug, el):
|
|||||||
# en même temps, mais un calcul dédié quand même pour rester correct si
|
# en même temps, mais un calcul dédié quand même pour rester correct si
|
||||||
# ça change un jour.
|
# ça change un jour.
|
||||||
data_binding_options = data_definition_options(slug, attributes.get("_data_definition_id"))
|
data_binding_options = data_definition_options(slug, attributes.get("_data_definition_id"))
|
||||||
|
# Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) : elle
|
||||||
|
# aussi garde SON objet dans un attribut séparé (_visibilite_cond_
|
||||||
|
# definition_id), jamais partagé avec le Répéteur/la Jauge/Donnée liée.
|
||||||
|
visibility_cond_options = data_definition_options(slug, attributes.get("_visibilite_cond_definition_id"))
|
||||||
|
global_variables = db.list_global_variables(slug)
|
||||||
result = []
|
result = []
|
||||||
for control in meta.get("controls", []):
|
for control in meta.get("controls", []):
|
||||||
control = dict(control)
|
control = dict(control)
|
||||||
if control["key"] in ("definition_id", "data_definition_id"):
|
if control["key"] in ("definition_id", "data_definition_id", "visibilite_cond_definition_id"):
|
||||||
defs = db.list_definitions(slug)
|
defs = db.list_definitions(slug)
|
||||||
control["options"] = [("", "(sélectionner un objet)")] + [(str(d["id"]), d["name"]) for d in defs]
|
control["options"] = [("", "(sélectionner un objet)")] + [(str(d["id"]), d["name"]) for d in defs]
|
||||||
|
if control["key"] == "visibilite_cond_champ":
|
||||||
|
control["options"] = [("", "(sélectionner un objet d'abord)")] + [(f["name"], _field_label(f)) for f in visibility_cond_options["fields"]]
|
||||||
|
if control["key"] == "visibilite_cond_variable":
|
||||||
|
control["options"] = [("", "(sélectionner une variable)")] + [(v["name"], v["name"]) for v in global_variables]
|
||||||
if control["key"] == "template_type_id":
|
if control["key"] == "template_type_id":
|
||||||
types = list_element_types(slug)
|
types = list_element_types(slug)
|
||||||
control["options"] = [("", "(aucun — utiliser le modèle de texte ci-dessous)")] + [(str(t["id"]), t["name"]) for t in types]
|
control["options"] = [("", "(aucun — utiliser le modèle de texte ci-dessous)")] + [(str(t["id"]), t["name"]) for t in types]
|
||||||
|
|||||||
+13
-6
@@ -362,12 +362,14 @@
|
|||||||
if (!screenData || !screenDiv) return;
|
if (!screenData || !screenDiv) return;
|
||||||
screenData.elements.filter(function(e){ return !e.parent_id; }).forEach(function(el) {
|
screenData.elements.filter(function(e){ return !e.parent_id; }).forEach(function(el) {
|
||||||
if (!el.rendered_html) return;
|
if (!el.rendered_html) return;
|
||||||
// "repeaterItem" (liste de données) et "jaugeBar" (2.1 — jauge liée
|
// "repeaterItem" (liste de données), "jaugeBar" (2.1 — jauge liée
|
||||||
// à une donnée) sont les deux seuls types d'éléments dont le
|
// à une donnée) et "visibilityGated" (condition de visibilité —
|
||||||
// contenu affiché dépend de la donnée elle-même plutôt que d'une
|
// voir render_element_html.py) sont les types d'éléments dont le
|
||||||
// action "Modifier un élément" posée à la main dans le graphe —
|
// contenu/l'affichage dépend de la donnée elle-même plutôt que
|
||||||
// eux seuls doivent être régénérés après un changement de donnée.
|
// d'une action "Modifier un élément" posée à la main dans le
|
||||||
if (el.rendered_html.indexOf('repeaterItem') === -1 && el.rendered_html.indexOf('jaugeBar') === -1) return;
|
// graphe — eux seuls doivent être régénérés après un changement
|
||||||
|
// de donnée ou de variable.
|
||||||
|
if (el.rendered_html.indexOf('repeaterItem') === -1 && el.rendered_html.indexOf('jaugeBar') === -1 && el.rendered_html.indexOf('visibilityGated') === -1) return;
|
||||||
const wrapper = screenDiv.querySelector('.playElement[data-el-id="' + el.id + '"] .playElementInner');
|
const wrapper = screenDiv.querySelector('.playElement[data-el-id="' + el.id + '"] .playElementInner');
|
||||||
if (wrapper) wrapper.innerHTML = el.rendered_html;
|
if (wrapper) wrapper.innerHTML = el.rendered_html;
|
||||||
});
|
});
|
||||||
@@ -534,6 +536,11 @@
|
|||||||
return fetch(runDataUrl, {method: 'POST'})
|
return fetch(runDataUrl, {method: 'POST'})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(() => refreshRuntimeData());
|
.then(() => refreshRuntimeData());
|
||||||
|
} else if (node.action_type === 'modifier_variable') {
|
||||||
|
const runVarUrl = "{{ url_for('flow_node_run_variable', slug=game.slug, node_id=999999) }}".replace('999999', node.id);
|
||||||
|
return fetch(runVarUrl, {method: 'POST'})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(() => refreshRuntimeData());
|
||||||
}
|
}
|
||||||
// 'rien' : aucune action volontairement.
|
// 'rien' : aucune action volontairement.
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
|||||||
@@ -343,7 +343,11 @@
|
|||||||
{% set ns.last_group = c.group %}
|
{% set ns.last_group = c.group %}
|
||||||
{% set ns.started = True %}
|
{% set ns.started = True %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="field controlRow">
|
<div class="field controlRow{{ ' visCondRow' if c.key.startswith('visibilite_cond_') and c.key != 'visibilite_cond_mode' else '' }}"
|
||||||
|
{% if c.key == 'visibilite_cond_variable' %}data-vis-cond-scope="variable"
|
||||||
|
{% elif c.key in ('visibilite_cond_definition_id', 'visibilite_cond_champ') %}data-vis-cond-scope="objet"
|
||||||
|
{% elif c.key in ('visibilite_cond_operateur', 'visibilite_cond_valeur') %}data-vis-cond-scope="any"
|
||||||
|
{% endif %}>
|
||||||
{% if c.type in ('checkbox', 'toggle_token') %}
|
{% if c.type in ('checkbox', 'toggle_token') %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
@@ -437,13 +441,16 @@
|
|||||||
{% elif c.type in ('select', 'preset') %}
|
{% elif c.type in ('select', 'preset') %}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select is-fullwidth">
|
<div class="select is-fullwidth">
|
||||||
<select name="ctrl_{{ c.key }}" id="field-{{ c.key }}">
|
<select name="ctrl_{{ c.key }}" id="field-{{ c.key }}" {% if c.key == 'visibilite_cond_mode' %}onchange="toggleVisCondFields(this)"{% endif %}>
|
||||||
{% for opt in c.options %}
|
{% for opt in c.options %}
|
||||||
<option value="{{ opt[0] }}" {{ 'selected' if c.value == opt[0] else '' }}>{{ opt[1] }}</option>
|
<option value="{{ opt[0] }}" {{ 'selected' if c.value == opt[0] else '' }}>{{ opt[1] }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if c.key == 'visibilite_cond_variable' %}
|
||||||
|
<button type="button" class="button is-small is-fullwidth" style="margin-top:6px;" onclick="createVisCondVariable()">+ Créer une nouvelle variable</button>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -618,6 +625,24 @@
|
|||||||
<input type="text" id="nf-action-data-value" style="margin-top:6px;" placeholder="Valeur / montant">
|
<input type="text" id="nf-action-data-value" style="margin-top:6px;" placeholder="Valeur / montant">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="nf-action-variable-fields" style="display:none;">
|
||||||
|
{% if not global_variables %}
|
||||||
|
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
|
||||||
|
{% else %}
|
||||||
|
<select id="nf-action-variable-name">
|
||||||
|
{% for v in global_variables %}
|
||||||
|
<option value="{{ v.name }}">{{ v.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<select id="nf-action-variable-operation" style="margin-top:6px;">
|
||||||
|
{% for key, label in data_operations %}
|
||||||
|
<option value="{{ key }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
|
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
|
||||||
@@ -1448,7 +1473,10 @@ function initBuilderPanel() {
|
|||||||
bindJaugeDefinitionSelect();
|
bindJaugeDefinitionSelect();
|
||||||
bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']);
|
bindDefinitionFieldSelects('field-definition_id', ['field-filtre_champ', 'field-filtre2_champ']);
|
||||||
bindDefinitionFieldSelects('field-data_definition_id', ['field-data_filtre_champ', 'field-data_filtre2_champ']);
|
bindDefinitionFieldSelects('field-data_definition_id', ['field-data_filtre_champ', 'field-data_filtre2_champ']);
|
||||||
|
bindDefinitionFieldSelects('field-visibilite_cond_definition_id', ['field-visibilite_cond_champ']);
|
||||||
initFilterValuePickers();
|
initFilterValuePickers();
|
||||||
|
var visCondModeSel = document.getElementById('field-visibilite_cond_mode');
|
||||||
|
if (visCondModeSel) toggleVisCondFields(visCondModeSel);
|
||||||
restoreTreeCollapsedState();
|
restoreTreeCollapsedState();
|
||||||
applySelectionHighlight(CURRENT_SELECTED_ID);
|
applySelectionHighlight(CURRENT_SELECTED_ID);
|
||||||
restoreFloatPanelState('props', 'propsPanelFloat', 'propsPanelReopenBtn');
|
restoreFloatPanelState('props', 'propsPanelFloat', 'propsPanelReopenBtn');
|
||||||
@@ -1497,6 +1525,41 @@ function bindJaugeDefinitionSelect() {
|
|||||||
// généralisé pour s'appliquer à un nombre quelconque de listes "champ"
|
// généralisé pour s'appliquer à un nombre quelconque de listes "champ"
|
||||||
// rattachées à un même sélecteur d'objet (ici : filtre_champ + filtre2_champ,
|
// rattachées à un même sélecteur d'objet (ici : filtre_champ + filtre2_champ,
|
||||||
// ou data_filtre_champ + data_filtre2_champ). ----------
|
// ou data_filtre_champ + data_filtre2_champ). ----------
|
||||||
|
// ---------- Condition de visibilité (voir VISIBILITY_CONDITION_CONTROLS) :
|
||||||
|
// n'affiche que les réglages pertinents pour le mode choisi (variable
|
||||||
|
// globale, ou champ d'un objet) — les deux autres restent en base (peu
|
||||||
|
// importe, seul le mode courant est lu par is_element_visible()) mais sont
|
||||||
|
// masqués pour ne pas embrouiller. ----------
|
||||||
|
function createVisCondVariable() {
|
||||||
|
var name = window.prompt("Nom de la nouvelle variable globale (ex. niveau_debloque) :");
|
||||||
|
if (!name || !name.trim()) return;
|
||||||
|
var body = new URLSearchParams({ name: name.trim(), type: 'texte', default_value: '' });
|
||||||
|
fetch("{{ url_for('create_global_var', slug=game.slug) }}", { method: 'POST', body: body })
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function (json) {
|
||||||
|
if (!json.ok) { window.alert(json.error || "Échec de la création."); return; }
|
||||||
|
var sel = document.getElementById('field-visibilite_cond_variable');
|
||||||
|
if (!sel) return;
|
||||||
|
if (!sel.querySelector('option[value="' + CSS.escape(json.name) + '"]')) {
|
||||||
|
var opt = document.createElement('option');
|
||||||
|
opt.value = json.name;
|
||||||
|
opt.textContent = json.name;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
sel.value = json.name;
|
||||||
|
submitPropsForm();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleVisCondFields(sel) {
|
||||||
|
var mode = sel.value;
|
||||||
|
document.querySelectorAll('.visCondRow').forEach(function (row) {
|
||||||
|
var scope = row.dataset.visCondScope;
|
||||||
|
var show = scope === 'any' ? !!mode : scope === mode;
|
||||||
|
row.style.display = show ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function bindDefinitionFieldSelects(defSelId, fieldSelIds) {
|
function bindDefinitionFieldSelects(defSelId, fieldSelIds) {
|
||||||
var defSel = document.getElementById(defSelId);
|
var defSel = document.getElementById(defSelId);
|
||||||
if (!defSel) return;
|
if (!defSel) return;
|
||||||
@@ -1769,6 +1832,9 @@ function nodeLabel(node) {
|
|||||||
} else if (node.action_type === 'modifier_donnee') {
|
} else if (node.action_type === 'modifier_donnee') {
|
||||||
base += ' → ' + (node.target_field || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
base += ' → ' + (node.target_field || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
||||||
if (node.data_value) base += ' ' + node.data_value;
|
if (node.data_value) base += ' ' + node.data_value;
|
||||||
|
} else if (node.action_type === 'modifier_variable') {
|
||||||
|
base += ' → ' + (node.target_variable || '?') + ' : ' + (DATA_OPERATION_LABELS_MAP[node.data_operation] || node.data_operation);
|
||||||
|
if (node.data_value) base += ' ' + node.data_value;
|
||||||
} else if (node.action_type === 'attendre') {
|
} else if (node.action_type === 'attendre') {
|
||||||
base += ' (' + (node.data_value || '0') + ' s)';
|
base += ' (' + (node.data_value || '0') + ' s)';
|
||||||
}
|
}
|
||||||
@@ -2052,11 +2118,13 @@ function toggleFlowActionFields() {
|
|||||||
var screenSel = document.getElementById('nf-action-target-screen');
|
var screenSel = document.getElementById('nf-action-target-screen');
|
||||||
var elementFields = document.getElementById('nf-action-element-fields');
|
var elementFields = document.getElementById('nf-action-element-fields');
|
||||||
var dataFields = document.getElementById('nf-action-data-fields');
|
var dataFields = document.getElementById('nf-action-data-fields');
|
||||||
|
var variableFields = document.getElementById('nf-action-variable-fields');
|
||||||
var waitFields = document.getElementById('nf-action-wait-fields');
|
var waitFields = document.getElementById('nf-action-wait-fields');
|
||||||
var isOnglet = typeSel.value === 'activer_onglet';
|
var isOnglet = typeSel.value === 'activer_onglet';
|
||||||
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none';
|
||||||
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? 'block' : 'none';
|
||||||
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
dataFields.style.display = typeSel.value === 'modifier_donnee' ? 'block' : 'none';
|
||||||
|
variableFields.style.display = typeSel.value === 'modifier_variable' ? 'block' : 'none';
|
||||||
waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
|
waitFields.style.display = typeSel.value === 'attendre' ? 'block' : 'none';
|
||||||
document.getElementById('nf-action-onglet-hint').style.display = isOnglet ? 'block' : 'none';
|
document.getElementById('nf-action-onglet-hint').style.display = isOnglet ? 'block' : 'none';
|
||||||
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
|
document.getElementById('nf-action-element-property-wrap').style.display = isOnglet ? 'none' : 'block';
|
||||||
@@ -2185,6 +2253,10 @@ function submitNodeForm() {
|
|||||||
payload.target_field = document.getElementById('nf-action-data-field').value;
|
payload.target_field = document.getElementById('nf-action-data-field').value;
|
||||||
payload.data_operation = document.getElementById('nf-action-data-operation').value;
|
payload.data_operation = document.getElementById('nf-action-data-operation').value;
|
||||||
payload.data_value = document.getElementById('nf-action-data-value').value;
|
payload.data_value = document.getElementById('nf-action-data-value').value;
|
||||||
|
} else if (actionType === 'modifier_variable') {
|
||||||
|
payload.target_variable = document.getElementById('nf-action-variable-name').value;
|
||||||
|
payload.data_operation = document.getElementById('nf-action-variable-operation').value;
|
||||||
|
payload.data_value = document.getElementById('nf-action-variable-value').value;
|
||||||
} else if (actionType === 'attendre') {
|
} else if (actionType === 'attendre') {
|
||||||
payload.data_value = document.getElementById('nf-action-wait-seconds').value;
|
payload.data_value = document.getElementById('nf-action-wait-seconds').value;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user