Ajoute les événements personnalisés (éditeur + exécution)
Deuxième moitié de la fonctionnalité "événements" (voir le commit précédent pour le backend) : un nouvel onglet "📣 Événements" dans screen_edit.html (scène ET modèle, même route) pour créer/modifier/ supprimer un événement (nom, description, "a un paramètre élément" oui/ non) et voir où il est déjà écouté/déclenché ; deux nouveaux types de nœud dans le graphe de logique ; exécution réelle côté client (play.html). - templates/screen_edit.html : - Onglet Événements : barre de création + tableau (patron form/name="..." + attribut `form="eventEditFormN"` déjà utilisé par l'onglet Variables du tableau de bord, pour éditer plusieurs champs d'une ligne sans emboîter un <form> dans un <tr>). Colonne "Utilisé par" avec liens directs vers les écrans/modèles concernés (list_custom_event_usages, déjà en place côté backend). - Nœud Déclencheur "Sur un événement personnalisé" : un simple sélecteur d'événement (trigger_custom_event_id) — ni élément ni écran, retrouvé par un scan global côté client (comme "À l'affichage de l'écran"). - Nœud Action "Déclencher un événement" : sélecteur d'événement (target_custom_event_id), avec élément/ligne concernés affichés seulement si l'événement a has_element_param (réutilise le sélecteur d'élément existant + "Ligne cliquée" pour la ligne, pas de définition d'objet ici donc pas de liste de lignes fixes possible). - Nœud Action "Modifier un élément" : nouvelle case "Utiliser l'élément transmis par l'événement en cours" (target_element_from_event) — v1, seule cette action l'expose (extensible plus tard sans nouveau changement de schéma). - nodeLabel()/submitNodeForm()/toggleFlowTriggerFields()/ toggleFlowActionFields() étendus en conséquence, CUSTOM_EVENTS_MAP (id -> nom/has_element_param) exposé côté JS pour le rendu des libellés et l'affichage conditionnel des champs paramètre. - templates/play.html : - window.dispatchGameEvent(eventId, elementId, rowId) : scan de gameData.flows (toutes scènes ET modèles à la fois, même principe que findTriggerNode()/runScreenShowTriggers()) pour trouver chaque écouteur, pose window.lastEventParams puis exécute son graphe (runFlowFrom) — au même titre que window.lastClickedRowId pour "Ligne cliquée". - runActionNode : nouvelle branche "declencher_evenement" (résout CLICKED_ROW_ID pour la ligne transmise, comme "Modifier une donnée") ; branche "modifier_element" étendue pour résoudre dynamiquement l'élément depuis window.lastEventParams quand target_element_from_event est actif. - readFieldValue (évaluation de condition) et l'appel serveur de "Modifier une donnée" (routes/flow/flow_node_run_data.py) résolvent désormais aussi EVENT_ROW_ID (-2), au même endroit que CLICKED_ROW_ID (-1) déjà en place. - routes/screens/screen_edit.py : passe custom_events/ custom_event_usages/custom_events_map_json au template (même patron que global_variables déjà threadé pour le sélecteur de variable dans le formulaire de Condition). Nouveaux tests dans tests/test_custom_events.py : forme exacte du payload runtime exposé au JS (types entiers, pas des chaînes — une comparaison stricte "===" échouerait silencieusement sinon), target_element_from_event bien persisté, résolution serveur d' EVENT_ROW_ID depuis le corps de la requête. 210 tests au total. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
dcbec16818
commit
f436190f90
@@ -13,14 +13,19 @@ def flow_node_run_data(slug, node_id):
|
|||||||
un nœud du graphe de logique plutôt que depuis la table _actions.
|
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
|
Si le nœud vise CLICKED_ROW_ID (voir "Ligne cliquée (Répéteur)" dans
|
||||||
screen_edit.html), la vraie ligne n'est connue qu'au moment du clic —
|
screen_edit.html) ou EVENT_ROW_ID (la ligne transmise par un événement
|
||||||
le client (play.html) la transmet dans le corps JSON de cette requête,
|
personnalisé en cours, voir screens/custom_events/), la vraie ligne
|
||||||
et elle remplace ici target_row_id avant d'exécuter l'action."""
|
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."""
|
||||||
node = screens.get_flow_node(slug, node_id)
|
node = screens.get_flow_node(slug, node_id)
|
||||||
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
|
if not node or node.get("node_type") != "action" or node.get("action_type") != "modifier_donnee":
|
||||||
return jsonify({"ok": False}), 400
|
return jsonify({"ok": False}), 400
|
||||||
if node.get("target_row_id") == screens.CLICKED_ROW_ID:
|
if node.get("target_row_id") == screens.CLICKED_ROW_ID:
|
||||||
body = request.get_json(silent=True) or {}
|
body = request.get_json(silent=True) or {}
|
||||||
node = {**node, "target_row_id": body.get("clicked_row_id")}
|
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)
|
ok = screens.apply_data_action(slug, node)
|
||||||
return jsonify({"ok": ok})
|
return jsonify({"ok": ok})
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ def screen_edit(slug, screen_id):
|
|||||||
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)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"screen_edit.html",
|
"screen_edit.html",
|
||||||
@@ -80,4 +86,6 @@ def screen_edit(slug, screen_id):
|
|||||||
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,
|
global_variables=global_variables,
|
||||||
|
custom_events=custom_events, custom_event_usages=custom_event_usages,
|
||||||
|
custom_events_map_json=json.dumps(custom_events_map),
|
||||||
)
|
)
|
||||||
|
|||||||
+54
-6
@@ -788,10 +788,25 @@
|
|||||||
bindHoverTexts();
|
bindHoverTexts();
|
||||||
bindHoverTriggers();
|
bindHoverTriggers();
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else if (node.action_type === 'modifier_element' && node.target_element_id) {
|
} else if (node.action_type === 'modifier_element' && (node.target_element_id || node.target_element_from_event)) {
|
||||||
const targetEl = document.querySelector('[data-element-id="' + node.target_element_id + '"]');
|
// 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).
|
||||||
|
const resolvedElementId = node.target_element_from_event
|
||||||
|
? (window.lastEventParams ? window.lastEventParams.element_id : null)
|
||||||
|
: node.target_element_id;
|
||||||
|
const targetEl = resolvedElementId ? document.querySelector('[data-element-id="' + resolvedElementId + '"]') : null;
|
||||||
if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value);
|
if (targetEl) applyElementProperty(targetEl, node.element_property, node.element_value);
|
||||||
return Promise.resolve();
|
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);
|
||||||
|
return Promise.resolve();
|
||||||
} else if (node.action_type === 'activer_onglet' && node.target_element_id) {
|
} else if (node.action_type === 'activer_onglet' && node.target_element_id) {
|
||||||
// 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre
|
// 2.3 — panneau à onglets / visibilité mutuellement exclusive : montre
|
||||||
// l'élément visé et masque tous ses FRÈRES DIRECTS (même parent_id,
|
// l'élément visé et masque tous ses FRÈRES DIRECTS (même parent_id,
|
||||||
@@ -827,12 +842,16 @@
|
|||||||
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
||||||
} else if (node.action_type === 'modifier_donnee') {
|
} 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);
|
const runDataUrl = "{{ url_for('flow_node_run_data', slug=game.slug, node_id=999999) }}".replace('999999', node.id);
|
||||||
// clicked_row_id : utilisé côté serveur SEULEMENT si ce nœud vise la
|
// clicked_row_id/event_row_id : utilisés côté serveur SEULEMENT si ce
|
||||||
// ligne cliquée (CLICKED_ROW_ID, voir flow_node_run_data.py) — sans
|
// nœud vise respectivement CLICKED_ROW_ID ou EVENT_ROW_ID (voir
|
||||||
// effet sinon, donc toujours transmis sans condition ici.
|
// flow_node_run_data.py) — sans effet sinon, donc toujours transmis
|
||||||
|
// sans condition ici.
|
||||||
return fetch(runDataUrl, {
|
return fetch(runDataUrl, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ clicked_row_id: window.lastClickedRowId })
|
body: JSON.stringify({
|
||||||
|
clicked_row_id: window.lastClickedRowId,
|
||||||
|
event_row_id: window.lastEventParams ? window.lastEventParams.row_id : null,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(() => refreshRuntimeData());
|
.then(() => refreshRuntimeData());
|
||||||
@@ -855,6 +874,12 @@
|
|||||||
// résolue seulement au moment de l'évaluation via le dernier clic
|
// résolue seulement au moment de l'évaluation via le dernier clic
|
||||||
// capturé (voir bindClicks() plus haut).
|
// capturé (voir bindClicks() plus haut).
|
||||||
if (rowId === -1) rowId = window.lastClickedRowId;
|
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 rows = gameData.data[String(definitionId)] || [];
|
||||||
const row = rows.find(r => r.id === rowId);
|
const row = rows.find(r => r.id === rowId);
|
||||||
return row ? row[fieldName] : undefined;
|
return row ? row[fieldName] : undefined;
|
||||||
@@ -1008,6 +1033,29 @@
|
|||||||
step(nodeId);
|
step(nodeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// é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) {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// L'écran affiché au démarrage doit être choisi AVANT le premier
|
// L'écran affiché au démarrage doit être choisi AVANT le premier
|
||||||
// bindClicks() : la recherche du nœud Déclencheur associé à un élément se
|
// bindClicks() : la recherche du nœud Déclencheur associé à un élément se
|
||||||
// fait sur l'écran courant (window.currentScreenId), qui doit donc déjà
|
// fait sur l'écran courant (window.currentScreenId), qui doit donc déjà
|
||||||
|
|||||||
+206
-8
@@ -172,6 +172,7 @@
|
|||||||
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Écran</button>
|
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Écran</button>
|
||||||
<button type="button" class="builderTabBtn" data-tab="flow" onclick="switchBuilderTab('flow')">🔀 Logique de la scène</button>
|
<button type="button" class="builderTabBtn" data-tab="flow" onclick="switchBuilderTab('flow')">🔀 Logique de la scène</button>
|
||||||
<button type="button" class="builderTabBtn" data-tab="anim" onclick="switchBuilderTab('anim')">🎬 Timeline d'animation</button>
|
<button type="button" class="builderTabBtn" data-tab="anim" onclick="switchBuilderTab('anim')">🎬 Timeline d'animation</button>
|
||||||
|
<button type="button" class="builderTabBtn" data-tab="events" onclick="switchBuilderTab('events')">📣 Événements</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="builderTabPanel active" data-tab-panel="screen">
|
<div class="builderTabPanel active" data-tab-panel="screen">
|
||||||
@@ -216,6 +217,16 @@
|
|||||||
</select></label>
|
</select></label>
|
||||||
<p class="hint" id="nf-trigger-repeater-hint" style="display:none;"></p>
|
<p class="hint" id="nf-trigger-repeater-hint" style="display:none;"></p>
|
||||||
<p class="hint" id="nf-trigger-affichage-hint" style="display:none;"></p>
|
<p class="hint" id="nf-trigger-affichage-hint" style="display:none;"></p>
|
||||||
|
<div id="nf-trigger-event-row" style="display:none;">
|
||||||
|
<label>Événement personnalisé<select id="nf-trigger-custom-event">
|
||||||
|
{% for ev in custom_events %}
|
||||||
|
<option value="{{ ev.id }}">{{ ev.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select></label>
|
||||||
|
{% 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>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="nodeFormCondition" style="display:none;">
|
<div id="nodeFormCondition" style="display:none;">
|
||||||
@@ -286,6 +297,10 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<div id="nf-action-element-fields" style="display:none;">
|
<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">
|
<select id="nf-action-target-element">
|
||||||
{% for e2 in elements %}
|
{% for e2 in elements %}
|
||||||
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
|
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
|
||||||
@@ -371,6 +386,31 @@
|
|||||||
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
|
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="nf-action-event-fields" style="display:none;">
|
||||||
|
{% 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()">
|
||||||
|
{% 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>
|
</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>
|
||||||
@@ -470,6 +510,76 @@
|
|||||||
<div id="animTracks" class="animTracks"></div>
|
<div id="animTracks" class="animTracks"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Onglet Événements : entité GAME-WIDE (pas propre à cet écran, voir
|
||||||
|
db/custom_events/) mais gérée depuis ici plutôt que le tableau de
|
||||||
|
bord — pour rester au plus près de l'endroit où on câble des
|
||||||
|
écouteurs/déclencheurs (nœuds Déclencheur/Action de la Logique de
|
||||||
|
la scène ci-dessus). Présent aussi sur un écran-modèle, comme les
|
||||||
|
autres onglets. -->
|
||||||
|
<div class="builderTabPanel" id="eventsTabPanel" data-tab-panel="events">
|
||||||
|
<div class="dashPanelHeader">
|
||||||
|
<h2>Événements personnalisés</h2>
|
||||||
|
<button type="button" class="button is-small primary" onclick="toggleDashCreate('events')">+ Créer un événement</button>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('custom_event_new', slug=game.slug) }}" class="dashCreateBar" id="dashCreate-events" style="display:none;">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{% if custom_events %}
|
||||||
|
{% for ev in custom_events %}
|
||||||
|
<form method="post" action="{{ url_for('custom_event_edit', slug=game.slug, event_id=ev.id) }}" id="eventEditForm{{ ev.id }}">
|
||||||
|
<input type="hidden" name="screen_id" value="{{ screen.id }}">
|
||||||
|
</form>
|
||||||
|
{% 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>
|
||||||
|
<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 %}
|
||||||
|
{% for u in usages %}
|
||||||
|
<a href="{{ url_for('screen_edit', slug=game.slug, screen_id=u.screen_id) }}" class="linkLikeButton" title="{{ 'Écouté ici' if u.role == 'ecoute' else 'Déclenché ici' }}">{{ '🔔' if u.role == 'ecoute' else '📣' }} {{ u.screen_name }}{{ ' (modèle)' if u.is_template else '' }}</a>{{ ', ' if not loop.last else '' }}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<span class="hint">Inutilisé</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="fieldsTableActions">
|
||||||
|
<button type="submit" form="eventEditForm{{ ev.id }}" class="button is-small" title="Enregistrer">💾</button>
|
||||||
|
<form method="post" action="{{ url_for('custom_event_delete', slug=game.slug, event_id=ev.id) }}"
|
||||||
|
onsubmit="return confirm('Supprimer l\'événement « {{ ev.name }} » ? Cela supprime aussi tous ses écouteurs et déclencheurs, sur toutes les scènes et tous les modèles.');">
|
||||||
|
<input type="hidden" name="screen_id" value="{{ screen.id }}">
|
||||||
|
<button type="submit" class="button is-small is-danger" title="Supprimer">🗑️</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="hint">Aucun événement pour l'instant — crée le premier ci-dessus, puis pose un nœud "Sur un événement personnalisé" (Déclencheur) ou "Déclencher un événement" (Action) dans la Logique de la scène.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Panneau droit : propriétés de l'élément sélectionné — flottant,
|
<!-- Panneau droit : propriétés de l'élément sélectionné — flottant,
|
||||||
@@ -2087,6 +2197,7 @@ var ELEMENT_DESACTIVE_LABELS_MAP = {{ element_desactive_labels_json|safe }};
|
|||||||
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
|
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
|
||||||
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
|
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
|
||||||
var TRIGGER_LABELS_MAP = {{ trigger_labels_json|safe }};
|
var TRIGGER_LABELS_MAP = {{ trigger_labels_json|safe }};
|
||||||
|
var CUSTOM_EVENTS_MAP = {{ custom_events_map_json|safe }};
|
||||||
|
|
||||||
var flowPendingPort = null; // {nodeId, port} en attente de connexion
|
var flowPendingPort = null; // {nodeId, port} en attente de connexion
|
||||||
|
|
||||||
@@ -2095,6 +2206,10 @@ function flowUrl(path) { return "/game/{{ game.slug }}" + path; }
|
|||||||
function nodeLabel(node) {
|
function nodeLabel(node) {
|
||||||
if (node.node_type === 'trigger') {
|
if (node.node_type === 'trigger') {
|
||||||
if (node.trigger_event === 'affichage') return '🖥️ ' + TRIGGER_LABELS_MAP['affichage'];
|
if (node.trigger_event === 'affichage') return '🖥️ ' + TRIGGER_LABELS_MAP['affichage'];
|
||||||
|
if (node.trigger_event === 'evenement') {
|
||||||
|
var listenedEvent = CUSTOM_EVENTS_MAP[node.trigger_custom_event_id];
|
||||||
|
return '📣 Sur « ' + (listenedEvent ? listenedEvent.name : '?') + ' »';
|
||||||
|
}
|
||||||
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
|
return '🖱️ ' + (TRIGGER_LABELS_MAP[node.trigger_event] || 'Au clic') + ' sur ' +
|
||||||
(ELEMENTS_LABELS[node.trigger_element_id] || '?');
|
(ELEMENTS_LABELS[node.trigger_element_id] || '?');
|
||||||
}
|
}
|
||||||
@@ -2126,7 +2241,8 @@ function nodeLabel(node) {
|
|||||||
} else if (node.action_type === 'activer_onglet') {
|
} else if (node.action_type === 'activer_onglet') {
|
||||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)';
|
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' (masque ses frères)';
|
||||||
} else if (node.action_type === 'modifier_element') {
|
} else if (node.action_type === 'modifier_element') {
|
||||||
base += ' → ' + (ELEMENTS_LABELS[node.target_element_id] || '?') + ' : ' + (ELEMENT_PROPERTY_LABELS_MAP[node.element_property] || node.element_property);
|
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);
|
||||||
if (node.element_property === 'visibilite') {
|
if (node.element_property === 'visibilite') {
|
||||||
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
|
base += ' = ' + (ELEMENT_VISIBILITY_LABELS_MAP[node.element_value] || node.element_value);
|
||||||
} else if (node.element_property === 'surbrillance') {
|
} else if (node.element_property === 'surbrillance') {
|
||||||
@@ -2147,6 +2263,12 @@ function nodeLabel(node) {
|
|||||||
if (node.data_value) base += ' ' + node.data_value;
|
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)';
|
||||||
|
} else if (node.action_type === 'declencher_evenement') {
|
||||||
|
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;
|
return base;
|
||||||
}
|
}
|
||||||
@@ -2324,10 +2446,16 @@ function openNodeForm(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleFlowTriggerFields() {
|
function toggleFlowTriggerFields() {
|
||||||
var isAffichage = document.getElementById('nf-trigger-event').value === 'affichage';
|
var eventVal = document.getElementById('nf-trigger-event').value;
|
||||||
document.getElementById('nf-trigger-element-row').style.display = isAffichage ? 'none' : 'block';
|
var isAffichage = eventVal === 'affichage';
|
||||||
document.getElementById('nf-trigger-repeater-hint').style.display = isAffichage ? 'none' : 'block';
|
// "Sur un événement personnalisé" ne concerne ni un élément précis ni
|
||||||
|
// l'écran entier : c'est un scan global (voir dispatchGameEvent() dans
|
||||||
|
// play.html), le seul réglage propre à ce nœud est QUEL événement.
|
||||||
|
var isCustomEvent = eventVal === 'evenement';
|
||||||
|
document.getElementById('nf-trigger-element-row').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block';
|
||||||
|
document.getElementById('nf-trigger-repeater-hint').style.display = (isAffichage || isCustomEvent) ? 'none' : 'block';
|
||||||
document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none';
|
document.getElementById('nf-trigger-affichage-hint').style.display = isAffichage ? 'block' : 'none';
|
||||||
|
document.getElementById('nf-trigger-event-row').style.display = isCustomEvent ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeNodeForm() {
|
function closeNodeForm() {
|
||||||
@@ -2491,16 +2619,53 @@ function toggleFlowActionFields() {
|
|||||||
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 variableFields = document.getElementById('nf-action-variable-fields');
|
||||||
var waitFields = document.getElementById('nf-action-wait-fields');
|
var waitFields = document.getElementById('nf-action-wait-fields');
|
||||||
|
var eventFields = document.getElementById('nf-action-event-fields');
|
||||||
var isOnglet = typeSel.value === 'activer_onglet';
|
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';
|
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 = (isModifierElement || 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';
|
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';
|
||||||
|
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-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';
|
||||||
if (typeSel.value === 'modifier_element') toggleFlowElementPropertyValue();
|
// 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_donnee') onFlowDefinitionChange('action');
|
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() {
|
function toggleFlowElementPropertyValue() {
|
||||||
@@ -2556,7 +2721,9 @@ function submitNodeForm() {
|
|||||||
|
|
||||||
if (currentNodeFormType === 'trigger') {
|
if (currentNodeFormType === 'trigger') {
|
||||||
payload.trigger_event = document.getElementById('nf-trigger-event').value;
|
payload.trigger_event = document.getElementById('nf-trigger-event').value;
|
||||||
if (payload.trigger_event !== 'affichage') {
|
if (payload.trigger_event === 'evenement') {
|
||||||
|
payload.trigger_custom_event_id = parseInt(document.getElementById('nf-trigger-custom-event').value, 10);
|
||||||
|
} else if (payload.trigger_event !== 'affichage') {
|
||||||
payload.trigger_element_id = parseInt(document.getElementById('nf-trigger-element').value, 10);
|
payload.trigger_element_id = parseInt(document.getElementById('nf-trigger-element').value, 10);
|
||||||
}
|
}
|
||||||
} else if (currentNodeFormType === 'condition') {
|
} else if (currentNodeFormType === 'condition') {
|
||||||
@@ -2601,7 +2768,11 @@ function submitNodeForm() {
|
|||||||
} else if (actionType === 'activer_onglet') {
|
} else if (actionType === 'activer_onglet') {
|
||||||
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);
|
||||||
} else if (actionType === 'modifier_element') {
|
} else if (actionType === 'modifier_element') {
|
||||||
payload.target_element_id = parseInt(document.getElementById('nf-action-target-element').value, 10);
|
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);
|
||||||
|
}
|
||||||
var prop = document.getElementById('nf-action-element-property').value;
|
var prop = document.getElementById('nf-action-element-property').value;
|
||||||
payload.element_property = prop;
|
payload.element_property = prop;
|
||||||
var isColorProp = prop === 'bg_color' || prop === 'border_color';
|
var isColorProp = prop === 'bg_color' || prop === 'border_color';
|
||||||
@@ -2639,6 +2810,15 @@ function submitNodeForm() {
|
|||||||
payload.data_value = document.getElementById('nf-action-variable-value').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;
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2671,6 +2851,24 @@ function switchBuilderTab(tab) {
|
|||||||
if (tab === 'flow') requestAnimationFrame(drawFlowEdges);
|
if (tab === 'flow') requestAnimationFrame(drawFlowEdges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleDashCreate(tab) {
|
||||||
|
var bar = document.getElementById('dashCreate-' + tab);
|
||||||
|
bar.style.display = bar.style.display === 'none' ? 'flex' : 'none';
|
||||||
|
if (bar.style.display === 'flex') {
|
||||||
|
var firstInput = bar.querySelector('input, select');
|
||||||
|
if (firstInput) firstInput.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Onglet initial : ?tab=... (utilisé par les routes de CRUD des
|
||||||
|
// événements, routes/custom_events/, pour revenir directement sur
|
||||||
|
// l'onglet "Événements" après création/modification/suppression plutôt
|
||||||
|
// que de laisser deviner où cliquer — même patron que game_dashboard.html).
|
||||||
|
(function () {
|
||||||
|
var requestedTab = {{ (request.args.get('tab') or '')|tojson }};
|
||||||
|
if (requestedTab) switchBuilderTab(requestedTab);
|
||||||
|
})();
|
||||||
|
|
||||||
// ---------- Timeline d'animation : piste par élément, clips Animate.css ou
|
// ---------- Timeline d'animation : piste par élément, clips Animate.css ou
|
||||||
// personnalisés (images-clés), glissés/redimensionnés à la souris sur un
|
// personnalisés (images-clés), glissés/redimensionnés à la souris sur un
|
||||||
// axe du temps en secondes depuis l'affichage de l'écran. Même principe de
|
// axe du temps en secondes depuis l'affichage de l'écran. Même principe de
|
||||||
|
|||||||
@@ -110,3 +110,88 @@ def test_deleting_an_event_cascades_to_every_referencing_node_everywhere(client,
|
|||||||
# Les écrans/modèles eux-mêmes ne sont pas touchés, seuls les nœuds.
|
# Les écrans/modèles eux-mêmes ne sont pas touchés, seuls les nœuds.
|
||||||
assert screens.get_screen(game, listener_screen) is not None
|
assert screens.get_screen(game, listener_screen) is not None
|
||||||
assert screens.get_screen(game, template_screen_id) is not None
|
assert screens.get_screen(game, template_screen_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_runtime_payload_exposes_both_nodes_for_the_client_js(client, game):
|
||||||
|
"""templates/play.html::dispatchGameEvent()/findTriggerNode() scannent
|
||||||
|
gameData.flows (toutes les scènes ET tous les modèles à la fois) pour
|
||||||
|
retrouver un nœud par trigger_custom_event_id/target_custom_event_id —
|
||||||
|
ce test verrouille que full_game_payload() expose bien ces deux champs
|
||||||
|
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)
|
||||||
|
|
||||||
|
listener_screen = _create_screen(client, game, "Écran écouteur")
|
||||||
|
listener_node_id = _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)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = screens.full_game_payload(game)
|
||||||
|
flows = payload["flows"]
|
||||||
|
|
||||||
|
listener_node = next(n for n in flows[str(listener_screen)]["nodes"] if n["id"] == listener_node_id)
|
||||||
|
assert listener_node["trigger_event"] == "evenement"
|
||||||
|
assert listener_node["trigger_custom_event_id"] == event_id
|
||||||
|
|
||||||
|
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