Dev #5
@@ -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()
|
||||
|
||||
@@ -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
|
||||
);
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -8,7 +8,6 @@ from core.flask_app import app
|
||||
@app.route("/game/<slug>/events/<int:event_id>/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"))
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
+19
-64
@@ -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); });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+18
-84
@@ -297,10 +297,6 @@
|
||||
</select>
|
||||
|
||||
<div id="nf-action-element-fields" style="display:none;">
|
||||
<label id="nf-action-event-param-row" style="display:none; align-items:center; gap:6px; margin-bottom:6px;">
|
||||
<input type="checkbox" id="nf-action-element-from-event" style="width:auto;" onchange="toggleFlowActionElementFromEvent()">
|
||||
Utiliser l'élément transmis par l'événement en cours (nœud "Sur un événement personnalisé")
|
||||
</label>
|
||||
<select id="nf-action-target-element">
|
||||
{% for e2 in elements %}
|
||||
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
|
||||
@@ -391,24 +387,16 @@
|
||||
{% if not custom_events %}
|
||||
<p class="hint">Aucun événement pour l'instant — <a href="javascript:void(0)" onclick="switchBuilderTab('events'); return false;">crées-en un dans l'onglet Événements</a>.</p>
|
||||
{% else %}
|
||||
<select id="nf-action-custom-event" onchange="toggleFlowEventDispatchFields()">
|
||||
<!-- Déclencher un événement = notifier, rien d'autre : aucun
|
||||
élément/ligne à choisir ici, c'est à l'ÉCOUTEUR (nœud
|
||||
Déclencheur "Sur un événement personnalisé", plus bas dans
|
||||
son propre graphe) de décider quoi faire, avec ses propres
|
||||
réglages habituels — voir screens/custom_events/. -->
|
||||
<select id="nf-action-custom-event">
|
||||
{% for ev in custom_events %}
|
||||
<option value="{{ ev.id }}">{{ ev.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<div id="nf-action-event-param-fields" style="margin-top:6px;">
|
||||
<label>Élément concerné<select id="nf-action-event-element">
|
||||
{% for e2 in elements %}
|
||||
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
|
||||
{% endfor %}
|
||||
</select></label>
|
||||
<label style="margin-top:6px;">Ligne concernée (si l'élément vient d'un Répéteur)
|
||||
<select id="nf-action-event-row">
|
||||
<option value="">Ne s'applique pas</option>
|
||||
<option value="-1">🖱️ Ligne cliquée (Répéteur)</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,10 +514,6 @@
|
||||
<input type="hidden" name="screen_id" value="{{ screen.id }}">
|
||||
<input type="text" name="name" placeholder="NOM (ex. NEED_ACTION)" required autocomplete="off">
|
||||
<input type="text" name="description" placeholder="Description (optionnel)">
|
||||
<label style="display:flex; align-items:center; gap:6px; white-space:nowrap; font-size:12.5px;">
|
||||
<input type="checkbox" name="has_element_param" style="width:auto;">
|
||||
Paramètre : élément (+ ligne de Répéteur)
|
||||
</label>
|
||||
<button type="submit" class="primary">Créer</button>
|
||||
</form>
|
||||
|
||||
@@ -541,18 +525,12 @@
|
||||
{% endfor %}
|
||||
<div class="fieldsTableWrap">
|
||||
<table class="table is-fullwidth is-hoverable fieldsTable">
|
||||
<thead><tr><th>Nom</th><th>Description</th><th>Paramètre</th><th>Utilisé par</th><th></th></tr></thead>
|
||||
<thead><tr><th>Nom</th><th>Description</th><th>Utilisé par</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
{% for ev in custom_events %}
|
||||
<tr>
|
||||
<td><code>{{ ev.name }}</code></td>
|
||||
<td><input type="text" form="eventEditForm{{ ev.id }}" name="description" value="{{ ev.description or '' }}" placeholder="Description" aria-label="Description de « {{ ev.name }} »"></td>
|
||||
<td>
|
||||
<label style="display:flex; align-items:center; gap:4px; white-space:nowrap;">
|
||||
<input type="checkbox" form="eventEditForm{{ ev.id }}" name="has_element_param" style="width:auto;" {{ 'checked' if ev.has_element_param else '' }}>
|
||||
élément
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
{% 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-70
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user