From 2c59e54556c1d08faf5fbcce031c29a24b7d0919 Mon Sep 17 00:00:00 2001 From: william Date: Sat, 29 Aug 2026 20:39:42 +0200 Subject: [PATCH] =?UTF-8?q?Simplifie=20les=20=C3=A9v=C3=A9nements=20:=20no?= =?UTF-8?q?tification=20pure,=20sans=20param=C3=A8tre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retour de l'utilisateur sur le premier jet : "Déclencher un événement" ne doit JAMAIS faire choisir un élément — c'est une notification pure, rien de plus. C'est à l'ÉCOUTEUR (déclencheur "Sur un événement personnalisé" → condition → action) de décider quoi faire ensuite, avec ses réglages habituels (cible fixe, "Ligne cliquée"...), jamais à l'événement de transporter un paramètre. Retire donc tout le mécanisme de transmission ajouté au tour précédent (has_element_param, target_element_from_event, EVENT_ROW_ID, window.lastEventParams) : - db/custom_events/ : _custom_events perd sa colonne has_element_param — un événement n'est plus qu'un nom + une description. - screens/flow/ : retire target_element_from_event (colonne ajoutée par ALTER TABLE, laissée inerte sur les bases déjà migrées — sans conséquence, plus jamais lue ni écrite) et la constante EVENT_ROW_ID. - routes/flow/flow_node_run_data.py : retire la résolution EVENT_ROW_ID, revient à sa forme d'origine (seul CLICKED_ROW_ID reste géré). - templates/screen_edit.html : le nœud Action "Déclencher un événement" n'a plus qu'un sélecteur d'événement — plus de champs élément/ligne. Le nœud Action "Modifier un élément" perd la case "Utiliser l'élément transmis par l'événement en cours". L'onglet Événements perd la case à cocher "Paramètre" (création et édition). - templates/play.html : window.dispatchGameEvent(eventId) ne prend plus que l'id de l'événement — scan global inchangé, mais ne pose plus aucun window.lastEventParams. modifier_element et readFieldValue reviennent à leur résolution d'origine (plus de branche event-aware). 208 tests au total (2 tests retirés, devenus sans objet : la persistance de target_element_from_event et la résolution serveur d'EVENT_ROW_ID). Co-Authored-By: Claude Sonnet 5 --- db/custom_events/create_custom_event.py | 6 +- .../ensure_custom_events_schema.py | 6 +- db/custom_events/update_custom_event.py | 7 +- routes/custom_events/custom_event_edit.py | 3 +- routes/custom_events/custom_event_new.py | 3 +- routes/flow/flow_node_run_data.py | 11 +- routes/screens/screen_edit.py | 5 +- screens/__init__.py | 2 +- screens/flow/constants.py | 27 ++--- screens/flow/ensure_flow_schema.py | 8 -- templates/play.html | 83 ++++---------- templates/screen_edit.html | 102 ++++-------------- tests/test_custom_events.py | 86 +++------------ 13 files changed, 76 insertions(+), 273 deletions(-) diff --git a/db/custom_events/create_custom_event.py b/db/custom_events/create_custom_event.py index e071bded..ce60dbb0 100644 --- a/db/custom_events/create_custom_event.py +++ b/db/custom_events/create_custom_event.py @@ -2,7 +2,7 @@ from ..connection import connect from .ensure_custom_events_schema import ensure_custom_events_schema -def create_custom_event(slug, name, description="", has_element_param=False): +def create_custom_event(slug, name, description=""): """Idempotent par nom (même convention que create_global_variable.py) : si le nom existe déjà, ne touche à rien et renvoie simplement son id existant plutôt que de lever une erreur — sans risque en cas de @@ -17,8 +17,8 @@ def create_custom_event(slug, name, description="", has_element_param=False): conn.close() return existing["id"] conn.execute( - "INSERT INTO _custom_events (name, description, has_element_param) VALUES (?, ?, ?)", - (name, (description or "").strip(), 1 if has_element_param else 0), + "INSERT INTO _custom_events (name, description) VALUES (?, ?)", + (name, (description or "").strip()), ) new_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"] conn.commit() diff --git a/db/custom_events/ensure_custom_events_schema.py b/db/custom_events/ensure_custom_events_schema.py index 8da80e61..d7bfaaaf 100644 --- a/db/custom_events/ensure_custom_events_schema.py +++ b/db/custom_events/ensure_custom_events_schema.py @@ -8,7 +8,10 @@ def ensure_custom_events_schema(slug): c'est par ce nom qu'on le désigne dans l'interface, même si les nœuds du graphe le référencent par id technique (voir screens/flow/ensure_flow_schema.py, trigger_custom_event_id/ - target_custom_event_id).""" + target_custom_event_id). Un événement est une pure NOTIFICATION, + sans paramètre : "déclencher" ne fait que signaler, jamais choisir + un élément — c'est à l'ÉCOUTEUR (déclencheur → condition → action) + de décider quoi faire, avec ses propres réglages habituels.""" conn = connect(slug) conn.execute( """ @@ -16,7 +19,6 @@ def ensure_custom_events_schema(slug): id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, description TEXT, - has_element_param INTEGER NOT NULL DEFAULT 0, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); """ diff --git a/db/custom_events/update_custom_event.py b/db/custom_events/update_custom_event.py index 839d6606..b548114d 100644 --- a/db/custom_events/update_custom_event.py +++ b/db/custom_events/update_custom_event.py @@ -2,7 +2,7 @@ from ..connection import connect from .ensure_custom_events_schema import ensure_custom_events_schema -def update_custom_event(slug, event_id, description="", has_element_param=False): +def update_custom_event(slug, event_id, description=""): """Le NOM reste volontairement immuable après création — comme une variable globale (voir update_global_variable.py) : c'est par ce nom qu'on désigne l'événement dans l'interface, mais surtout par son ID @@ -11,9 +11,6 @@ def update_custom_event(slug, event_id, description="", has_element_param=False) casserait donc rien niveau logique.""" ensure_custom_events_schema(slug) conn = connect(slug) - conn.execute( - "UPDATE _custom_events SET description = ?, has_element_param = ? WHERE id = ?", - ((description or "").strip(), 1 if has_element_param else 0, event_id), - ) + conn.execute("UPDATE _custom_events SET description = ? WHERE id = ?", ((description or "").strip(), event_id)) conn.commit() conn.close() diff --git a/routes/custom_events/custom_event_edit.py b/routes/custom_events/custom_event_edit.py index ea8ec496..e5d821c5 100644 --- a/routes/custom_events/custom_event_edit.py +++ b/routes/custom_events/custom_event_edit.py @@ -8,7 +8,6 @@ from core.flask_app import app @app.route("/game//events//edit", methods=["POST"]) def custom_event_edit(slug, event_id): description = request.form.get("description") or "" - has_element_param = request.form.get("has_element_param") == "on" screen_id = request.form.get("screen_id", type=int) - db.update_custom_event(slug, event_id, description, has_element_param) + db.update_custom_event(slug, event_id, description) return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, tab="events")) diff --git a/routes/custom_events/custom_event_new.py b/routes/custom_events/custom_event_new.py index d2468475..6d27aa71 100644 --- a/routes/custom_events/custom_event_new.py +++ b/routes/custom_events/custom_event_new.py @@ -14,8 +14,7 @@ def custom_event_new(slug): bon écran ensuite, jamais stocké sur l'événement lui-même (game-wide).""" name = (request.form.get("name") or "").strip() description = request.form.get("description") or "" - has_element_param = request.form.get("has_element_param") == "on" screen_id = request.form.get("screen_id", type=int) if name: - db.create_custom_event(slug, name, description, has_element_param) + db.create_custom_event(slug, name, description) return redirect(url_for("screen_edit", slug=slug, screen_id=screen_id, tab="events")) diff --git a/routes/flow/flow_node_run_data.py b/routes/flow/flow_node_run_data.py index ef233cd9..0d362df8 100644 --- a/routes/flow/flow_node_run_data.py +++ b/routes/flow/flow_node_run_data.py @@ -13,19 +13,14 @@ def flow_node_run_data(slug, node_id): un nœud du graphe de logique plutôt que depuis la table _actions. Si le nœud vise CLICKED_ROW_ID (voir "Ligne cliquée (Répéteur)" dans - screen_edit.html) ou EVENT_ROW_ID (la ligne transmise par un événement - personnalisé en cours, voir screens/custom_events/), la vraie ligne - n'est connue qu'au moment de l'exécution — le client (play.html) la - transmet dans le corps JSON de cette requête, et elle remplace ici - target_row_id avant d'exécuter l'action.""" + screen_edit.html), la vraie ligne n'est connue qu'au moment du clic — + le client (play.html) la transmet dans le corps JSON de cette requête, + et elle remplace ici target_row_id avant d'exécuter l'action.""" node = screens.get_flow_node(slug, node_id) if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee": return jsonify({"ok": False}), 400 if node.get("target_row_id") == screens.CLICKED_ROW_ID: body = request.get_json(silent=True) or {} node = {**node, "target_row_id": body.get("clicked_row_id")} - elif node.get("target_row_id") == screens.EVENT_ROW_ID: - body = request.get_json(silent=True) or {} - node = {**node, "target_row_id": body.get("event_row_id")} ok = screens.apply_data_action(slug, node) return jsonify({"ok": ok}) diff --git a/routes/screens/screen_edit.py b/routes/screens/screen_edit.py index d134fb25..05b3e59b 100644 --- a/routes/screens/screen_edit.py +++ b/routes/screens/screen_edit.py @@ -46,10 +46,7 @@ def screen_edit(slug, screen_id): global_variables = db.list_global_variables(slug) custom_events = db.list_custom_events(slug) custom_event_usages = screens.list_custom_event_usages(slug) - custom_events_map = { - e["id"]: {"name": e["name"], "has_element_param": bool(e["has_element_param"])} - for e in custom_events - } + custom_events_map = {e["id"]: {"name": e["name"]} for e in custom_events} return render_template( "screen_edit.html", diff --git a/screens/__init__.py b/screens/__init__.py index 36706271..6cd09235 100644 --- a/screens/__init__.py +++ b/screens/__init__.py @@ -56,7 +56,7 @@ from .labels.element_visibility import ELEMENT_VISIBILITY_VALUES, ELEMENT_VISIBI from .labels.data_operations import DATA_OPERATIONS, DATA_OPERATION_LABELS from .flow.constants import ( - TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, EVENT_ROW_ID, + TRIGGER_EVENTS, CONDITION_OPERATORS, CONDITION_OPERATOR_LABELS, CLICKED_ROW_ID, ) from .rendering.render_element_html import render_element_html diff --git a/screens/flow/constants.py b/screens/flow/constants.py index 23a087ab..41b407b3 100644 --- a/screens/flow/constants.py +++ b/screens/flow/constants.py @@ -21,16 +21,6 @@ # AUTOINCREMENT SQLite). CLICKED_ROW_ID = -1 -# Même principe que CLICKED_ROW_ID, pour un second contexte dynamique : -# "la ligne de Répéteur transmise par l'événement personnalisé en cours -# d'exécution" (voir screens/custom_events/, nœud Déclencheur -# trigger_event="evenement") — stockable directement dans target_row_id -# (déjà sans contrainte de clé étrangère, voir ensure_flow_schema.py) au -# même titre que CLICKED_ROW_ID, résolu côté client (templates/play.html) -# via window.lastEventParams.row_id au lieu de window.lastClickedRowId. -# Valeur distincte de CLICKED_ROW_ID pour ne jamais les confondre. -EVENT_ROW_ID = -2 - TRIGGER_EVENTS = [ ("clic", "Au clic"), ("soumission", "À la soumission"), @@ -89,14 +79,11 @@ FLOW_NODE_FIELDS = { "target_variable", # Événements personnalisés (voir screens/custom_events/) : quel # événement un nœud Déclencheur écoute (trigger_event="evenement") ou - # un nœud Action déclenche (action_type="declencher_evenement") — - # target_element_id/target_row_id (déjà ci-dessus, EVENT_ROW_ID - # compris) portent les paramètres transmis au déclenchement. - # target_element_from_event : indicateur réutilisable par N'IMPORTE - # QUEL nœud Action qui accepte déjà target_element_id (v1 : seule - # "modifier_element" le supporte) — contourne la contrainte de clé - # étrangère de target_element_id (impossible d'y stocker un sentinel - # comme EVENT_ROW_ID sur target_row_id) en résolvant "l'élément - # transmis par l'événement en cours" côté client à la place. - "trigger_custom_event_id", "target_custom_event_id", "target_element_from_event", + # un nœud Action déclenche (action_type="declencher_evenement") — un + # événement est une pure NOTIFICATION, sans paramètre : "déclencher" + # ne fait que signaler, jamais choisir un élément. C'est à l'ÉCOUTEUR + # (déclencheur → condition → action) de décider quoi faire ensuite, + # avec ses propres réglages habituels (target_element_id normal, pas + # de mécanisme dynamique dédié). + "trigger_custom_event_id", "target_custom_event_id", } diff --git a/screens/flow/ensure_flow_schema.py b/screens/flow/ensure_flow_schema.py index aeae3bdf..4f59eea3 100644 --- a/screens/flow/ensure_flow_schema.py +++ b/screens/flow/ensure_flow_schema.py @@ -83,13 +83,5 @@ def ensure_flow_schema(slug): conn.execute("ALTER TABLE _flow_nodes ADD COLUMN trigger_custom_event_id INTEGER") if "target_custom_event_id" not in cols: conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_custom_event_id INTEGER") - if "target_element_from_event" not in cols: - # Indicateur réutilisable par n'importe quel nœud Action qui - # accepte déjà target_element_id (v1 : seule "modifier_element") — - # contourne la contrainte de clé étrangère de target_element_id - # (impossible d'y stocker un sentinel comme EVENT_ROW_ID sur - # target_row_id) en résolvant "l'élément transmis par l'événement - # en cours" côté client (templates/play.html) à la place. - conn.execute("ALTER TABLE _flow_nodes ADD COLUMN target_element_from_event INTEGER NOT NULL DEFAULT 0") conn.commit() conn.close() diff --git a/templates/play.html b/templates/play.html index fb68b1be..a0509424 100644 --- a/templates/play.html +++ b/templates/play.html @@ -788,46 +788,16 @@ bindHoverTexts(); bindHoverTriggers(); return Promise.resolve(); - } else if (node.action_type === 'modifier_element' && (node.target_element_id || node.target_element_from_event)) { - // target_element_from_event (voir screens/custom_events/) : résout - // dynamiquement "l'élément transmis par l'événement en cours" - // (window.lastEventParams, posé par dispatchGameEvent() juste avant - // d'exécuter ce graphe) plutôt qu'une cible fixe choisie dans - // l'éditeur — contourne la contrainte de clé étrangère de - // target_element_id, qui empêche d'y stocker un sentinel comme - // EVENT_ROW_ID directement (voir screens/flow/ensure_flow_schema.py). - // - // data-element-id N'EST PAS unique quand l'élément vit dans un - // Répéteur : chaque ligne rejoue le MÊME modèle, donc le MÊME - // data-element-id se répète à l'identique sur chaque ligne (voir - // render_repeater.py, seul data-row-id — posé sur .repeaterItem, - // l'englobant — distingue réellement une ligne d'une autre). Un - // simple document.querySelector('[data-element-id]') global - // prendrait toujours la PREMIÈRE ligne, jamais la bonne — sans - // rapport avec l'événement reçu. Quand l'événement transmet aussi - // une ligne (row_id), la recherche est donc scopée à L'INTÉRIEUR de - // ce .repeaterItem précis ; sinon (élément fixe, ou événement sans - // paramètre de ligne), recherche globale comme avant. - const resolvedElementId = node.target_element_from_event - ? (window.lastEventParams ? window.lastEventParams.element_id : null) - : node.target_element_id; - const resolvedRowId = node.target_element_from_event && window.lastEventParams - ? window.lastEventParams.row_id : null; - let targetEl = null; - if (resolvedElementId != null) { - if (resolvedRowId != null) { - const rowEl = document.querySelector('.repeaterItem[data-row-id="' + resolvedRowId + '"]'); - targetEl = rowEl ? rowEl.querySelector('[data-element-id="' + resolvedElementId + '"]') : null; - } else { - targetEl = document.querySelector('[data-element-id="' + resolvedElementId + '"]'); - } - } + } else if (node.action_type === 'modifier_element' && node.target_element_id) { + const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]'); if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value); return Promise.resolve(); } else if (node.action_type === 'declencher_evenement' && node.target_custom_event_id) { - let dispatchRowId = node.target_row_id; - if (dispatchRowId === -1) dispatchRowId = window.lastClickedRowId; // CLICKED_ROW_ID - window.dispatchGameEvent(node.target_custom_event_id, node.target_element_id || null, dispatchRowId != null ? dispatchRowId : null); + // Notification pure (voir screens/custom_events/) : ne fait que + // signaler l'événement, jamais choisir un élément — c'est à + // l'écouteur (déclencheur → condition → action) de décider quoi + // faire, avec ses propres réglages habituels. + window.dispatchGameEvent(node.target_custom_event_id); return Promise.resolve(); } else if (node.action_type === 'activer_onglet' && node.target_element_id) { // 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre @@ -864,16 +834,12 @@ return new Promise(function(resolve) { setTimeout(resolve, ms); }); } else if (node.action_type === 'modifier_donnee') { const runDataUrl = "{{ url_for('flow_node_run_data', slug=game.slug, node_id=999999) }}".replace('999999', node.id); - // clicked_row_id/event_row_id : utilisés côté serveur SEULEMENT si ce - // nœud vise respectivement CLICKED_ROW_ID ou EVENT_ROW_ID (voir - // flow_node_run_data.py) — sans effet sinon, donc toujours transmis - // sans condition ici. + // clicked_row_id : utilisé côté serveur SEULEMENT si ce nœud vise la + // ligne cliquée (CLICKED_ROW_ID, voir flow_node_run_data.py) — sans + // effet sinon, donc toujours transmis sans condition ici. return fetch(runDataUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - clicked_row_id: window.lastClickedRowId, - event_row_id: window.lastEventParams ? window.lastEventParams.row_id : null, - }) + body: JSON.stringify({ clicked_row_id: window.lastClickedRowId }) }) .then(r => r.json()) .then(() => refreshRuntimeData()); @@ -896,12 +862,6 @@ // résolue seulement au moment de l'évaluation via le dernier clic // capturé (voir bindClicks() plus haut). if (rowId === -1) rowId = window.lastClickedRowId; - // EVENT_ROW_ID (-2, voir screens/flow/constants.py) : "la ligne de - // Répéteur transmise par l'événement personnalisé en cours" — même - // principe que CLICKED_ROW_ID ci-dessus, résolue via - // window.lastEventParams (posé par dispatchGameEvent()) plutôt que le - // dernier clic capturé. - if (rowId === -2) rowId = window.lastEventParams ? window.lastEventParams.row_id : null; const rows = gameData.data[String(definitionId)] || []; const row = rows.find(r => r.id === rowId); return row ? row[fieldName] : undefined; @@ -1056,25 +1016,20 @@ } // Déclenche un événement personnalisé (voir screens/custom_events/, - // action "declencher_evenement") : scan GLOBAL de gameData.flows, même - // principe que findTriggerNode()/runScreenShowTriggers() ci-dessus — un + // action "declencher_evenement") : une pure NOTIFICATION, sans aucun + // paramètre — scan GLOBAL de gameData.flows, même principe que + // findTriggerNode()/runScreenShowTriggers() ci-dessus, puisqu'un // écouteur (nœud Déclencheur trigger_event="evenement") peut vivre sur // N'IMPORTE QUELLE autre scène ou modèle, pas seulement l'écran affiché. - // elementId/rowId (potentiellement null si l'événement n'a pas de - // paramètre) sont posés sur window.lastEventParams AVANT de lancer - // chaque graphe trouvé, pour que les nœuds en aval (ex. "Modifier un - // élément" avec target_element_from_event, voir applyElementProperty - // plus haut/runActionNode plus bas) puissent le résoudre — au même - // titre que window.lastClickedRowId pour "Ligne cliquée (Répéteur)". - window.dispatchGameEvent = function(eventId, elementId, rowId) { + // C'est à CHAQUE écouteur de décider quoi faire ensuite (via sa propre + // suite condition/action, avec ses réglages habituels — cible fixe, + // "Ligne cliquée"...), jamais à l'événement lui-même de le dicter. + window.dispatchGameEvent = function(eventId) { for (const screenIdKey in gameData.flows) { const flow = gameData.flows[screenIdKey]; flow.nodes .filter(function(n){ return n.node_type === 'trigger' && n.trigger_event === 'evenement' && n.trigger_custom_event_id === eventId; }) - .forEach(function(n) { - window.lastEventParams = { element_id: elementId, row_id: rowId }; - runFlowFrom(n.id, screenIdKey); - }); + .forEach(function(n) { runFlowFrom(n.id, screenIdKey); }); } }; diff --git a/templates/screen_edit.html b/templates/screen_edit.html index a3139b57..273e5e72 100644 --- a/templates/screen_edit.html +++ b/templates/screen_edit.html @@ -297,10 +297,6 @@ @@ -526,10 +514,6 @@ - @@ -541,18 +525,12 @@ {% endfor %}
- + {% for ev in custom_events %} -
NomDescriptionParamètreUtilisé par
NomDescriptionUtilisé par
{{ ev.name }} - - {% set usages = custom_event_usages.get(ev.id, []) %} {% if usages %} @@ -2241,8 +2219,7 @@ function nodeLabel(node) { } else if (node.action_type === 'activer_onglet') { base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)'; } else if (node.action_type === 'modifier_element') { - var elementLabel = node.target_element_from_event ? "l'élément transmis par l'événement" : (ELEMENTS_LABELS[node.target_element_id] || '?'); - base += ' → ' + elementLabel + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property); + base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property); if (node.element_property === 'visibilite') { base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value); } else if (node.element_property === 'surbrillance') { @@ -2264,11 +2241,11 @@ function nodeLabel(node) { } else if (node.action_type === 'attendre') { base += ' (' + (node.data_value || '0') + ' s)'; } else if (node.action_type === 'declencher_evenement') { + // Notification pure : rien d'autre à afficher que le nom de + // l'événement (voir screens/custom_events/) — aucun élément/ligne + // n'est jamais transmis au déclenchement. var dispatchedEvent = CUSTOM_EVENTS_MAP[node.target_custom_event_id]; base += ' « ' + (dispatchedEvent ? dispatchedEvent.name : '?') + ' »'; - if (dispatchedEvent && dispatchedEvent.has_element_param && node.target_element_id) { - base += ' (' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ')'; - } } return base; } @@ -2621,51 +2598,16 @@ function toggleFlowActionFields() { var waitFields = document.getElementById('nf-action-wait-fields'); var eventFields = document.getElementById('nf-action-event-fields'); var isOnglet = typeSel.value === 'activer_onglet'; - var isModifierElement = typeSel.value === 'modifier_element'; screenSel.style.display = (typeSel.value === 'aller_a' || typeSel.value === 'ouvrir_ligne') ? 'block' : 'none'; - elementFields.style.display = (isModifierElement || isOnglet) ? 'block' : 'none'; + elementFields.style.display = (typeSel.value === 'modifier_element' || isOnglet) ? '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'; if (eventFields) eventFields.style.display = typeSel.value === 'declencher_evenement' ? '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'; - // v1 : seule "Modifier un élément" sait résoudre "l'élément transmis par - // l'événement en cours" (target_element_from_event) — pas encore - // "Activer cet onglet" (voir le plan, extensible plus tard sans nouveau - // changement de schéma). - var eventParamRow = document.getElementById('nf-action-event-param-row'); - if (eventParamRow) eventParamRow.style.display = isModifierElement ? 'flex' : 'none'; - if (!isModifierElement) document.getElementById('nf-action-element-from-event').checked = false; - if (isModifierElement) { toggleFlowElementPropertyValue(); toggleFlowActionElementFromEvent(); } + if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue(); if (typeSel.value === 'modifier_donnee') onFlowDefinitionChange('action'); - if (typeSel.value === 'declencher_evenement') toggleFlowEventDispatchFields(); -} - -// "Utiliser l'élément transmis par l'événement en cours" (voir -// nf-action-event-param-row ci-dessus) : quand coché, le sélecteur -// d'élément fixe n'a plus de sens (target_element_from_event résout -// dynamiquement l'élément côté client, voir play.html) — masqué plutôt -// que laissé visible mais ignoré, pour ne pas laisser croire qu'il compte. -function toggleFlowActionElementFromEvent() { - var checked = document.getElementById('nf-action-element-from-event').checked; - document.getElementById('nf-action-target-element').style.display = checked ? 'none' : 'block'; -} - -// L'élément/la ligne concernés ne sont utiles que si l'événement choisi a -// réellement un paramètre (voir has_element_param, CUSTOM_EVENTS_MAP) — -// sans ça, rien à transmettre au déclenchement (voir target_element_id/ -// target_row_id du nœud Action "declencher_evenement"). Pas de "définition -// d'objet" ici (contrairement à "Modifier une donnée") : la ligne -// concernée ne peut donc être que "Ligne cliquée" (CLICKED_ROW_ID, si ce -// nœud est lui-même exécuté suite à un clic sur une ligne de Répéteur) ou -// "ne s'applique pas" — jamais une ligne fixe choisie dans une liste. -function toggleFlowEventDispatchFields() { - var sel = document.getElementById('nf-action-custom-event'); - var paramFields = document.getElementById('nf-action-event-param-fields'); - if (!sel || !paramFields) return; - var eventInfo = CUSTOM_EVENTS_MAP[sel.value]; - paramFields.style.display = (eventInfo && eventInfo.has_element_param) ? 'block' : 'none'; } function toggleFlowElementPropertyValue() { @@ -2768,11 +2710,7 @@ function submitNodeForm() { } else if (actionType === 'activer_onglet') { payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10); } else if (actionType === 'modifier_element') { - var fromEvent = document.getElementById('nf-action-element-from-event').checked; - payload.target_element_from_event = fromEvent ? 1 : 0; - if (!fromEvent) { - payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10); - } + payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10); var prop = document.getElementById('nf-action-element-property').value; payload.element_property = prop; var isColorProp = prop === 'bg_color' || prop === 'border_color'; @@ -2811,14 +2749,10 @@ function submitNodeForm() { } else if (actionType === 'attendre') { payload.data_value = document.getElementById('nf-action-wait-seconds').value; } else if (actionType === 'declencher_evenement') { - var evId = parseInt(document.getElementById('nf-action-custom-event').value, 10); - payload.target_custom_event_id = evId; - var evInfo = CUSTOM_EVENTS_MAP[evId]; - if (evInfo && evInfo.has_element_param) { - payload.target_element_id = parseInt(document.getElementById('nf-action-event-element').value, 10); - var evRow = document.getElementById('nf-action-event-row').value; - if (evRow) payload.target_row_id = parseInt(evRow, 10); - } + // Notification pure : aucun élément/ligne à transmettre (voir + // screens/custom_events/) — c'est à l'écouteur de décider quoi + // faire, avec ses propres réglages habituels. + payload.target_custom_event_id = parseInt(document.getElementById('nf-action-custom-event').value, 10); } } diff --git a/tests/test_custom_events.py b/tests/test_custom_events.py index 1966ec2a..b8c53ce5 100644 --- a/tests/test_custom_events.py +++ b/tests/test_custom_events.py @@ -4,7 +4,13 @@ un événement game-wide (ex. NEED_ACTION) écouté par un nœud Déclencheur un nœud Action ("action_type"="declencher_evenement") sur UNE AUTRE scène/UN AUTRE modèle — la seule entité game-wide du moteur dont la suppression cascade RÉELLEMENT vers les nœuds qui la référencent, -partout à la fois (voir screens/custom_events/delete_custom_event.py).""" +partout à la fois (voir screens/custom_events/delete_custom_event.py). + +Un événement est une pure NOTIFICATION, sans aucun paramètre : le +déclencher ne fait que signaler, jamais choisir un élément — c'est à +l'ÉCOUTEUR (déclencheur → condition → action) de décider quoi faire +ensuite, avec ses propres réglages habituels (cible fixe, "Ligne +cliquée"...).""" import json import re @@ -17,11 +23,6 @@ def _create_screen(client, slug, name="Accueil"): return int(re.search(r"/screens/(\d+)/edit", resp.headers["Location"]).group(1)) -def _add_element(client, slug, screen_id, widget="bouton"): - resp = client.post(f"/game/{slug}/screens/{screen_id}/elements/add", data={"widget": widget}, follow_redirects=False) - return int(re.search(r"selected=(\d+)", resp.headers["Location"]).group(1)) - - def _add_flow_node(client, slug, screen_id, **fields): resp = client.post( f"/game/{slug}/screens/{screen_id}/flow/nodes/add", @@ -35,43 +36,39 @@ def _add_flow_node(client, slug, screen_id, **fields): def test_create_event_is_idempotent_by_name(client, game): screen_id = _create_screen(client, game) resp = client.post(f"/game/{game}/events/new", data={ - "name": "NEED_ACTION", "description": "Besoin d'action", "has_element_param": "on", "screen_id": screen_id, + "name": "NEED_ACTION", "description": "Besoin d'action", "screen_id": screen_id, }) assert resp.status_code == 302 events = db.list_custom_events(game) assert len(events) == 1 assert events[0]["name"] == "NEED_ACTION" - assert events[0]["has_element_param"] == 1 # Double soumission : ne crée pas un deuxième événement du même nom. client.post(f"/game/{game}/events/new", data={"name": "NEED_ACTION", "screen_id": screen_id}) assert len(db.list_custom_events(game)) == 1 -def test_edit_event_updates_description_and_param_flag(client, game): +def test_edit_event_updates_description(client, game): screen_id = _create_screen(client, game) - event_id = db.create_custom_event(game, "NEED_ACTION", "old", False) + event_id = db.create_custom_event(game, "NEED_ACTION", "old") resp = client.post(f"/game/{game}/events/{event_id}/edit", data={ - "description": "new", "has_element_param": "on", "screen_id": screen_id, + "description": "new", "screen_id": screen_id, }) assert resp.status_code == 302 event = db.get_custom_event(game, event_id) assert event["description"] == "new" - assert event["has_element_param"] == 1 def test_usages_reports_listener_and_dispatcher_across_different_screens(client, game): - event_id = db.create_custom_event(game, "NEED_ACTION", "", True) + event_id = db.create_custom_event(game, "NEED_ACTION") listener_screen = _create_screen(client, game, "Écran écouteur") _add_flow_node(client, game, listener_screen, node_type="trigger", trigger_event="evenement", trigger_custom_event_id=event_id) dispatcher_screen = _create_screen(client, game, "Écran déclencheur") - element_id = _add_element(client, game, dispatcher_screen) _add_flow_node( client, game, dispatcher_screen, - node_type="action", action_type="declencher_evenement", - target_custom_event_id=event_id, target_element_id=element_id, + node_type="action", action_type="declencher_evenement", target_custom_event_id=event_id, ) usages = screens.list_custom_event_usages(game) @@ -82,7 +79,7 @@ def test_usages_reports_listener_and_dispatcher_across_different_screens(client, def test_deleting_an_event_cascades_to_every_referencing_node_everywhere(client, game): - event_id = db.create_custom_event(game, "NEED_ACTION", "", True) + event_id = db.create_custom_event(game, "NEED_ACTION") listener_screen = _create_screen(client, game, "Écran écouteur") listener_node_id = _add_flow_node( @@ -120,7 +117,7 @@ def test_runtime_payload_exposes_both_nodes_for_the_client_js(client, game): intacts pour chaque écran, avec le bon type (entier, pas une chaîne), faute de quoi une comparaison stricte "===" côté JS échouerait silencieusement.""" - event_id = db.create_custom_event(game, "NEED_ACTION", "", True) + event_id = db.create_custom_event(game, "NEED_ACTION") listener_screen = _create_screen(client, game, "Écran écouteur") listener_node_id = _add_flow_node( @@ -129,11 +126,9 @@ def test_runtime_payload_exposes_both_nodes_for_the_client_js(client, game): ) dispatcher_screen = _create_screen(client, game, "Écran déclencheur") - element_id = _add_element(client, game, dispatcher_screen) dispatcher_node_id = _add_flow_node( client, game, dispatcher_screen, - node_type="action", action_type="declencher_evenement", - target_custom_event_id=event_id, target_element_id=element_id, + node_type="action", action_type="declencher_evenement", target_custom_event_id=event_id, ) payload = screens.full_game_payload(game) @@ -146,52 +141,3 @@ def test_runtime_payload_exposes_both_nodes_for_the_client_js(client, game): dispatcher_node = next(n for n in flows[str(dispatcher_screen)]["nodes"] if n["id"] == dispatcher_node_id) assert dispatcher_node["action_type"] == "declencher_evenement" assert dispatcher_node["target_custom_event_id"] == event_id - assert dispatcher_node["target_element_id"] == element_id - - -def test_modifier_element_from_event_flag_persists(client, game): - screen_id = _create_screen(client, game) - node_id = _add_flow_node( - client, game, screen_id, - node_type="action", action_type="modifier_element", - target_element_from_event=1, element_property="surbrillance", element_value="pulse", - ) - node = screens.get_flow_node(game, node_id) - assert node["target_element_from_event"] == 1 - assert node["target_element_id"] is None - - -def test_flow_node_run_data_resolves_event_row_id_from_request_body(client, game): - """Miroir serveur du sentinel EVENT_ROW_ID (voir play.html, - readFieldValue) — flow_node_run_data.py doit résoudre target_row_id - depuis le corps JSON ("event_row_id") exactement comme il le fait déjà - pour CLICKED_ROW_ID ("clicked_row_id").""" - resp = client.post(f"/game/{game}/objects/new", data={ - "object_name": "Jauge", "field_name[]": ["valeur"], - "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]) - client.post(f"/game/{game}/objects/{def_id}/data/new", data={"valeur": "1"}) - client.post(f"/game/{game}/objects/{def_id}/data/new", data={"valeur": "2"}) - rows = db.list_rows(game, db.get_definition(game, def_id)) - row_id = next(r["id"] for r in rows if r["valeur"] == 2) - - screen_id = _create_screen(client, game) - node_id = _add_flow_node( - client, game, screen_id, - node_type="action", action_type="modifier_donnee", - target_definition_id=def_id, target_row_id=screens.EVENT_ROW_ID, - target_field="valeur", data_operation="definir_nombre", data_value="99", - ) - - resp = client.post( - f"/game/{game}/flow/nodes/{node_id}/run-data", - data=json.dumps({"clicked_row_id": None, "event_row_id": row_id}), - content_type="application/json", - ) - assert resp.status_code == 200 - assert resp.get_json()["ok"] is True - - row = db.get_row(game, db.get_definition(game, def_id), row_id) - assert row["valeur"] == 99