Files
Forge-Engine/templates/screen_edit.html
T
williamandClaude Sonnet 5 76331f9285
Build and deploy / test-python (push) Successful in 1m27s
Build and deploy / test-js (push) Successful in 6s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Phase 6 : son (musique de fond par écran + action "Jouer un son")
Ajoute deux mécanismes distincts, à la demande de l'utilisateur qui a
précisé qu'un son doit pouvoir être attaché à une SCÈNE (pas seulement
joué ponctuellement par une action de flow, comme prévu initialement) :

- Musique de fond par écran (nouveau champ background_music_url sur
  _screens, ALTER TABLE nullable) : réglée dans le panneau gauche de
  l'éditeur (URL ou envoi de fichier, réutilise la route d'upload
  générique existante), enregistrée en AJAX au même patron que le format
  d'aperçu (screen_set_aspect.py). Démarrée en boucle à l'affichage de
  l'écran et arrêtée au changement d'écran (runScreenBackgroundMusic(),
  appelée depuis showScreen() dans static/js/play/screens.js) — un seul
  Audio actif à la fois, jamais cumulé avec une musique restée d'un écran
  précédent.

- Action de flow "jouer_son" (aux côtés des actions existantes) : effet
  sonore ponctuel, non bouclé, déclenchable sur n'importe quel nœud
  Déclencheur. Réutilise data_value (déjà un champ texte générique sur
  le nœud action, comme pour "attendre") plutôt qu'une nouvelle colonne
  dédiée — fire-and-forget côté client (runActionNode), ne bloque jamais
  la suite du graphe.

Les deux réutilisent le même mécanisme d'upload de fichier déjà en place
ailleurs dans l'éditeur (ex. source d'une vidéo), sans nouvelle route.

Ceci complète les 6 phases du plan d'extension du moteur (état par
joueur, hasard/maths, clavier/minuteur, ajout de ligne, position/
collision, son) : Forge Engine peut désormais couvrir des jeux bien
au-delà du narratif/puzzle/quiz (action, arcade, jeux à contrainte de
temps).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-30 17:56:49 +02:00

1177 lines
68 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ screen.name }} — {{ game.name }}{% endblock %}
{% block content_class %}content-builder{% endblock %}
{% block body_class %}builderBody{% endblock %}
{% block extra_head %}
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fonts/fonts.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/animate.min.css') }}">
{% endblock %}
{% block content %}
{% if screen.is_template %}
<p class="hint" style="background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:10px 14px; margin:10px 0;">
🧩 Contenu d'un élément de jeu réutilisable.
</p>
{% endif %}
{% macro render_tree_node(el) %}
{% set kids = children_of.get(el.id, []) %}
<div class="treeNode" data-element-id="{{ el.id }}">
<div class="treeRow" draggable="true" data-parent-id="{{ el.parent_id or '' }}" data-is-container="{{ '1' if el.widget in container_widgets else '' }}">
{% if kids %}
<button type="button" class="treeToggle" data-node-id="{{ el.id }}" aria-label="Réduire / déplier">▾</button>
{% else %}
<span class="treeToggleSpacer"></span>
{% endif %}
<a class="treeRowLink {{ 'selected' if el.id == selected_id else '' }}"
href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=el.id) }}"
data-element-id="{{ el.id }}"
oncontextmenu="return showElementContextMenu(event, {{ el.id }});">
<span class="treeIcon">{{ widgets.get(el.widget, {}).get('icon', '❔') }}</span>
<span class="treeLabel">{{ el|elabel }}</span>
</a>
</div>
{% if kids %}
<div class="treeChildren">
{% for child in kids %}
{{ render_tree_node(child) }}
{% endfor %}
</div>
{% endif %}
</div>
{% endmacro %}
<div class="builder3" id="builder3">
{% set el = elements|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
<!-- Panneau gauche : ajouter un élément (icônes, sans jargon technique) —
flottant, déplaçable, fermable, même mécanisme que le panneau droit
(voir floatPanel*() plus bas). -->
<button type="button" id="panelLeftReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openLeftPanel()" title="Rouvrir le panneau des éléments">🧩 Éléments</button>
<div class="floatPanel floatPanel--left" id="panelLeftFloat">
<div class="floatPanelHeader" data-drag-panel="panelLeftFloat" data-drag-key="left">
<span class="floatPanelTitle">⋮⋮ Éléments de cet écran</span>
<button type="button" class="button is-small" onclick="closeLeftPanel()" title="Fermer">✕</button>
</div>
<div class="floatPanelBody">
<!-- Nom de l'écran, aperçu jouable, et format de l'aperçu (Portrait/
Paysage/Carré) : regroupés ici plutôt qu'au-dessus du canevas —
ce sont des réglages qu'on touche rarement une fois l'écran en
cours de construction, pas besoin qu'ils prennent en permanence de
la place dans la zone centrale. -->
<form method="post" action="{{ url_for('screen_rename', slug=game.slug, screen_id=screen.id) }}" class="field has-addons" style="align-items:center; margin-bottom:8px;">
<div class="control is-expanded"><input class="input is-small" type="text" name="name" value="{{ screen.name }}" required></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
{% if not screen.is_template %}
<a class="button is-small is-primary is-fullwidth mb-2" href="{{ url_for('game_play', slug=game.slug) }}" target="_blank">▶️ Jouer depuis le début</a>
{% endif %}
{% set current_aspect = screen.preview_aspect or '9/16' %}
<div class="buttons has-addons are-small mb-2" style="flex-wrap:nowrap;">
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '9/16' else '' }}" data-aspect="9/16">📱 Portrait</button>
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '16/9' else '' }}" data-aspect="16/9">🖥️ Paysage</button>
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '1/1' else '' }}" data-aspect="1/1">◻️ Carré</button>
</div>
<!-- Phase 6 — musique de fond de cet écran : démarrée en boucle à
l'affichage, arrêtée au changement d'écran (voir
runScreenBackgroundMusic() dans static/js/play/screens.js).
Enregistrement en AJAX au blur/changement, même patron que le
format d'aperçu ci-dessus. -->
<div class="field" style="margin-bottom:8px;">
<label class="label is-small" for="screen-bg-music-url">🎵 Musique de fond</label>
<div class="control">
<input type="text" class="input is-small" id="screen-bg-music-url" placeholder="URL du fichier audio" value="{{ screen.background_music_url or '' }}">
</div>
<div class="control" style="margin-top:4px;">
<label for="screen-bg-music-upload" style="font-size:12px;">Envoyer un fichier :</label>
<input type="file" class="screenUploadInput" id="screen-bg-music-upload" data-target="screen-bg-music-url" accept="audio/*" style="font-size:12px;">
<span class="uploadStatus" style="font-size:12px; color:var(--muted);"></span>
</div>
</div>
<hr style="border-color:var(--border); margin:10px 0;">
<h2 class="subtitle is-6">Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
{% for el in top_level_elements %}
{{ render_tree_node(el) }}
{% endfor %}
</div>
{% else %}
<p class="hint">Aucun élément pour l'instant.</p>
{% endif %}
<details class="propGroup">
<summary>Ajouter un élément sur l'écran</summary>
<div class="propGroupBody">
<div class="widgetGrid">
{% for wkey, w in widgets.items() if wkey != 'icone' %}
<form method="post" action="{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}" class="widgetBtnForm">
<input type="hidden" name="widget" value="{{ wkey }}">
<button type="submit" class="widgetBtn" title="{{ w.label }}">
<span class="widgetIcon">{{ w.icon }}</span>
<span class="widgetLabel">{{ w.label }}</span>
</button>
</form>
{% endfor %}
</div>
</div>
</details>
<!-- Galerie d'icônes — pas un widget parmi d'autres dans la grille
ci-dessus (voir "if wkey != 'icone'") : cliquer/glisser une icône ICI
pose directement un élément Icône déjà réglé sur cette icône précise,
plutôt qu'un élément générique qu'il faudrait ensuite configurer. -->
<details class="propGroup">
<summary>🔤 Icônes</summary>
<div class="propGroupBody">
<input type="text" class="input is-small" id="iconGallerySearch" placeholder="Rechercher une icône..." oninput="filterIconGallery(this.value)">
<div class="iconGallery" id="iconGallery">
{% for slug in fontawesome_icons %}
<button type="button" class="iconGalleryTile" draggable="true"
data-icon-class="{{ slug }}" data-icon-search="{{ slug }}"
title="{{ slug }}" onclick="addIconElement('{{ slug }}')">
<i class="icon-svg" style="--icon-url:url('/static/icons/{{ slug }}.svg');"></i>
</button>
{% endfor %}
</div>
<p class="help">Glisse une icône sur le canevas, ou clique dessus pour l'ajouter — sa couleur et sa taille se règlent ensuite dans ses propriétés ("Icône"). Sélection non exhaustive : d'autres icônes pourront être ajoutées à cette liste plus tard si besoin.</p>
</div>
</details>
<!-- Menu rétractable dédié aux éléments de jeu personnalisés (Personnage,
Outil, IA...) : chacun a désormais SA PROPRE tuile, avec son nom,
plutôt qu'une unique tuile générique "Élément de jeu" qui ajoutait
toujours le premier du catalogue sans laisser choisir lequel. -->
<details class="propGroup">
<summary>🧩 Mes éléments de jeu{{ ' (' ~ element_types|length ~ ')' if element_types else '' }}</summary>
<div class="propGroupBody">
{% if element_types %}
<div class="widgetGrid">
{% for et in element_types %}
<form method="post" action="{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}" class="widgetBtnForm">
<input type="hidden" name="widget" value="__catalogue__">
<input type="hidden" name="element_type_id" value="{{ et.id }}">
<button type="submit" class="widgetBtn" title="{{ et.name }}">
<span class="widgetIcon">{{ et.icon or '🧩' }}</span>
<span class="widgetLabel">{{ et.name }}</span>
</button>
</form>
{% endfor %}
</div>
{% else %}
<p class="hint">Pas encore d'élément de jeu personnalisé — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='element_types') }}">crées-en un</a>.</p>
{% endif %}
</div>
</details>
</div>
</div>
<!-- Menu contextuel (clic droit sur un élément de l'arborescence) -->
<div id="elementContextMenu" class="contextMenu" style="display:none;">
<button type="button" onclick="contextMenuDuplicate()">📄 Dupliquer</button>
</div>
<!-- Centre : l'écran (onglet "Écran"), ou la logique de la scène / la
timeline d'animation quand leur onglet est actif — un seul onglet
visible à la fois, chacun occupant tout l'espace disponible (voir
switchBuilderTab() plus bas). Présents aussi sur un écran-modèle
(élément de jeu réutilisable, ex. "mail content") : ses enfants
gardent désormais TOUJOURS le même id, à chaque exemplaire posé sur
n'importe quelle scène (rechargés en direct depuis ce modèle à
chaque affichage, voir _render_element_type_children côté serveur —
l'ancien mécanisme qui les copiait avec un nouvel id à chaque pose,
instantiate_template_tree, a été retiré). Un déclencheur/une
animation posé ici, sur un enfant du modèle, s'exécute donc bien
partout où ce modèle est utilisé (voir findTriggerNode/
collectAnimationClips dans templates/play.html). -->
<div class="builderCanvasArea">
{% set current_aspect = screen.preview_aspect or '9/16' %}
<div class="builderTabs" id="builderTabs">
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Écran</button>
<button type="button" class="builderTabBtn" data-tab="blocks" onclick="switchBuilderTab('blocks')">🧩 Blocs de logique</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 class="builderTabPanel active" data-tab-panel="screen">
<div class="canvasFrame">
<div class="canvas" id="canvas" style="aspect-ratio:{{ current_aspect }};">
{% for el in top_level_elements %}
<div class="canvasElement {{ 'selected' if el.id == selected_id else '' }}" id="el-{{ el.id }}" style="{{ el|elstyle }}" data-anim-target="{{ el.id }}"
onmousedown="onElementMouseDown(event, {{ el.id }})">
<div class="canvasElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div>
<div class="resizeHandle" onmousedown="onResizeMouseDown(event, {{ el.id }})"></div>
</div>
{% endfor %}
</div>
</div>
<p class="hint" style="text-align:center;">Glisse pour déplacer, tire le coin pour redimensionner, clique pour sélectionner.</p>
</div>
<div class="builderTabPanel" id="blocksTabPanel" data-tab-panel="blocks">
<div class="dashPanelHeader">
<h2>Blocs de logique</h2>
<button type="button" class="button is-small primary" onclick="toggleDashCreate('blocks')">+ Créer un bloc</button>
</div>
<form method="post" action="{{ url_for('flow_block_new', slug=game.slug, screen_id=screen.id) }}" class="dashCreateBar" id="dashCreate-blocks" style="display:none;">
<input type="text" name="name" placeholder="Nom du bloc (ex. Ouvrir la porte)" required autocomplete="off">
<input type="text" name="description" placeholder="Description (optionnel)">
<button type="submit" class="primary">Créer</button>
</form>
{% if blocks_view %}
{% for b in blocks_view %}
<form method="post" action="{{ url_for('flow_block_edit', slug=game.slug, screen_id=screen.id, block_id=b.id) }}" id="blockEditForm{{ b.id }}"></form>
{% endfor %}
<div class="fieldsTableWrap">
<table class="table is-fullwidth is-hoverable fieldsTable">
<thead><tr><th>Nom</th><th>Description</th><th>Éléments concernés</th><th>Nœuds</th><th></th></tr></thead>
<tbody>
{% for b in blocks_view %}
<tr>
<td><strong>{{ b.name }}</strong></td>
<td>{{ b.description|truncatesentences(3) }}</td>
<td>{% if b.element_labels %}{{ b.element_labels|join(', ') }}{% else %}<span class="hint">—</span>{% endif %}</td>
<td>{{ b.node_count }}</td>
<td class="fieldsTableActions">
<button type="button" class="button is-small is-primary" onclick="openLogicBlockPanel({{ b.id }})" title="Ouvrir le graphe de ce bloc">🧩 Ouvrir</button>
<button type="button" class="button is-small" onclick="toggleBlockEditRow({{ b.id }})" title="Modifier le nom/la description">✏️</button>
<form method="post" action="{{ url_for('flow_block_delete', slug=game.slug, screen_id=screen.id, block_id=b.id) }}"
onsubmit="return confirm('Supprimer le bloc « {{ b.name }} » ? Cela supprime aussi ses {{ b.node_count }} nœud(s) et leurs connexions.');" style="display:inline;">
<button type="submit" class="button is-small is-danger" title="Supprimer">🗑️</button>
</form>
</td>
</tr>
<tr class="hidden" id="blockEditRow{{ b.id }}">
<td colspan="5">
<div class="dashCreateBar" style="display:flex;">
<input type="text" form="blockEditForm{{ b.id }}" name="name" value="{{ b.name }}" required>
<input type="text" form="blockEditForm{{ b.id }}" name="description" value="{{ b.description or '' }}" placeholder="Description">
<button type="submit" form="blockEditForm{{ b.id }}" class="primary">Enregistrer</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="hint">Aucun bloc pour l'instant — crée le premier ci-dessus, puis ouvre-le pour poser des déclencheurs/conditions/actions.</p>
{% endif %}
</div>
<!-- Modale plein écran/redimensionnable/déplaçable d'un bloc de logique
(voir openLogicBlockPanel()) — patron makeFloatPanelDraggable/
Resizable/Fullscreenable, identique à game_dashboard.html. Un seul
bloc ouvert à la fois : ce même #nodeForm/#flowCanvas est réutilisé
pour n'importe quel bloc (voir CURRENT_BLOCK_ID). -->
<div class="floatPanel floatPanel--center hidden" id="logicBlockPanel">
<div class="floatPanelHeader" id="logicBlockPanelHeader">
<span class="floatPanelTitle">⋮⋮ <span id="logicBlockPanelTitle">Bloc</span></span>
<button type="button" class="button is-small floatPanelFullscreenBtn" title="Plein écran">⛶</button>
<button type="button" class="button is-small" onclick="closeLogicBlockPanel()">✕</button>
</div>
<div class="floatPanelBody logicBlockPanelBody">
<div class="flowToolbar">
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('condition')">➕ Condition</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('action')">➕ Action</button>
<span class="hint">Clique un point de sortie d'un nœud, puis le point d'entrée d'un autre nœud, pour les relier. Reclique une connexion pour la supprimer.</span>
</div>
<div id="nodeForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="nodeFormTitle">Nouveau nœud</strong>
<button type="button" class="smallBtn button is-small" onclick="closeNodeForm()">✕</button>
</div>
<div id="nodeFormTrigger" style="display:none;">
<label>Événement<select id="nf-trigger-event" onchange="toggleFlowTriggerFields()">
{% for key, label in trigger_events %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label id="nf-trigger-element-row">Élément<select id="nf-trigger-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select></label>
<p class="hint" id="nf-trigger-repeater-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 id="nf-trigger-key-row" style="display:none;">
<label>Touche à surveiller
<input type="text" id="nf-trigger-key-display" readonly placeholder="Clique ici puis appuie sur une touche" onkeydown="captureFlowTriggerKey(event)">
</label>
<input type="hidden" id="nf-trigger-key">
</div>
<div id="nf-trigger-interval-row" style="display:none;">
<label>Toutes les... millisecondes
<input type="number" id="nf-trigger-interval" min="50" step="50" value="1000" placeholder="ex. 1000">
</label>
</div>
</div>
<div id="nodeFormCondition" style="display:none;">
<label>Source<select id="nf-cond-source" onchange="onFlowConditionSourceChange()">
<option value="objet">Champ d'un objet</option>
<option value="variable">Variable globale</option>
<option value="collision">Collision entre deux éléments</option>
</select></label>
<div id="nf-cond-objet-fields" {% if not definitions %}style="display:none;"{% endif %}>
<label>Objet<select id="nf-cond-definition" onchange="onFlowDefinitionChange('cond')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select></label>
<label>Ligne concernée<select id="nf-cond-row"></select></label>
<label>Champ<select id="nf-cond-field" onchange="onFlowFieldChange()"></select></label>
</div>
{% if not definitions %}
<p class="hint" id="nf-cond-no-objet-hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='objects') }}">crées-en un</a>.</p>
{% endif %}
<div id="nf-cond-variable-fields" style="display:none;">
<label>Variable<select id="nf-cond-variable">
<option value="">(sélectionner une variable)</option>
{% for v in global_variables %}
<option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %}
</select></label>
<!-- Utile seulement si la variable choisie est de type
"objet"/"tableau" : sans chemin, c'est toute la variable
(son JSON) qui est comparée. -->
<label>Chemin (optionnel)<input type="text" id="nf-cond-variable-chemin" placeholder="ex. .nom ou [0].valeur"></label>
{% if not global_variables %}
<p class="hint">Aucune variable globale pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='variables') }}" target="_blank">crées-en une ↗</a></p>
{% endif %}
</div>
<div id="nf-cond-collision-fields" style="display:none;">
<label>Premier élément<select id="nf-cond-element-a">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select></label>
<label>Deuxième élément<select id="nf-cond-element-b">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select></label>
<p class="hint">Vrai si les deux élément se touchent (leurs rectangles se chevauchent à l'écran) — relie le port "Faux" du nœud pour "ne se touchent pas".</p>
</div>
<div id="nf-cond-operator-value-row">
<label>Condition<select id="nf-cond-operator">
{% for key, label in condition_operators %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label>Valeur de comparaison<input type="text" id="nf-cond-value" placeholder="ex. 1, vrai, 10..."></label>
</div>
<div id="nf-cond-extra-wrap">
<label id="nf-cond-combinator-row" style="display:none;">Combiner les conditions avec
<select id="nf-cond-combinator">
<option value="et">ET (toutes les conditions doivent être vraies)</option>
<option value="ou">OU (au moins une condition doit être vraie)</option>
</select>
</label>
<div id="nf-cond-extra-clauses"></div>
<button type="button" class="smallBtn button is-small" onclick="addFlowConditionClause()">+ Ajouter une condition</button>
</div>
</div>
<div id="nodeFormAction" style="display:none;">
<label>Type d'action<select id="nf-action-type" onchange="toggleFlowActionFields()">
{% for key, label in action_type_labels.items() %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<select id="nf-action-target-screen" style="display:none;">
{% for s in all_screens %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
<div id="nf-action-element-fields" style="display:none;">
<select id="nf-action-target-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select>
<p class="hint" id="nf-action-onglet-hint" style="display:none;"></p>
<div id="nf-action-element-property-wrap">
<select id="nf-action-element-property" onchange="toggleFlowElementPropertyValue()" style="margin-top:6px;">
{% for key, label in element_action_properties %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-visibility" style="margin-top:6px;">
{% for key, label in element_visibility_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-surbrillance" style="display:none; margin-top:6px;">
{% for key, label in element_surbrillance_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-desactive" style="display:none; margin-top:6px;">
{% for key, label in element_desactive_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="color" id="nf-action-element-value-color" style="display:none; margin-top:6px;" value="#5b8cff">
<input type="number" id="nf-action-element-value-number" style="display:none; margin-top:6px;" value="50" placeholder="Nouvelle valeur en %">
<input type="text" id="nf-action-element-value-text" style="display:none; margin-top:6px;" placeholder="Nouveau texte affiché">
<label id="nf-action-element-toggle-row" style="display:flex; align-items:center; gap:6px; margin-top:10px; font-size:12px; color:var(--text-dim);">
<input type="checkbox" id="nf-action-element-toggle" onchange="toggleFlowElementToggleMode()" style="width:auto; margin:0;">
Va-et-vient : au 1er clic, passer à cette valeur ; au 2e clic, revenir à la précédente ; etc.
</label>
<div id="nf-action-element-value2-wrap" style="display:none; margin-top:6px;">
<input type="color" id="nf-action-element-value2-color" style="display:none;" value="#ff0000">
<input type="number" id="nf-action-element-value2-number" style="display:none;" placeholder="Valeur au 2e état en %">
</div>
</div>
</div>
<div id="nf-action-wait-fields" style="display:none;">
<label>Délai en secondes avant de continuer
<input type="number" id="nf-action-wait-seconds" min="0" step="0.5" value="3" placeholder="ex. 3">
</label>
</div>
<div id="nf-action-data-fields" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='objects') }}">crées-en un</a>.</p>
{% else %}
<select id="nf-action-data-definition" onchange="onFlowDefinitionChange('action')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
<select id="nf-action-data-row" style="margin-top:6px;"></select>
<select id="nf-action-data-field" style="margin-top:6px;"></select>
<select id="nf-action-data-operation" style="margin-top:6px;" onchange="updateDataValuePlaceholder('nf-action-data-operation', 'nf-action-data-value')">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-data-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
<div id="nf-action-add-row-fields" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='objects') }}">crées-en un</a>.</p>
{% else %}
<select id="nf-action-add-row-definition">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
<p class="hint">Crée une ligne vide dans cet objet. Enchaîne ensuite un ou plusieurs nœuds "Modifier une donnée" ciblant "➕ Dernière ligne ajoutée" (Ligne concernée) pour renseigner ses champs.</p>
{% endif %}
</div>
<!-- Phase 6 — effet sonore ponctuel (non bouclé, joué une seule
fois), sur n'importe quel déclencheur. Indépendant de la
musique de fond par écran (voir screen_edit.html, panneau
gauche) qui, elle, boucle et suit le cycle de vie de l'écran. -->
<div id="nf-action-sound-fields" style="display:none;">
<label>URL du son
<input type="text" id="nf-action-sound-url" placeholder="URL du fichier audio">
</label>
<div style="margin-top:6px;">
<label for="nf-action-sound-upload" style="font-size:12px;">Envoyer un fichier :</label>
<input type="file" class="nodeFormUploadInput" id="nf-action-sound-upload" data-target="nf-action-sound-url" accept="audio/*" style="font-size:12px;">
<span class="uploadStatus" style="font-size:12px; color:var(--text-dim);"></span>
</div>
</div>
<div id="nf-action-variable-fields" style="display:none;">
{% if not global_variables %}
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
{% else %}
<select id="nf-action-variable-name">
{% for v in global_variables %}
<option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %}
</select>
<select id="nf-action-variable-operation" style="margin-top:6px;" onchange="updateDataValuePlaceholder('nf-action-variable-operation', 'nf-action-variable-value')">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
<div 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 %}
<!-- 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>
{% endif %}
</div>
</div>
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
</div>
<div class="flowCanvasWrap">
<svg id="flowEdges" class="flowEdgesSvg"></svg>
<div id="flowCanvas" class="flowCanvas"></div>
</div>
</div>
<div class="floatPanelResizeHandle" id="logicBlockPanelResizeHandle"></div>
</div>
<div class="builderTabPanel" id="animTabPanel" data-tab-panel="anim">
<div class="flowToolbar">
<label style="display:flex; align-items:center; gap:6px;">
Piste
<select id="anim-new-track-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }}</option>
{% endfor %}
</select>
</label>
<button type="button" class="smallBtn button is-small" onclick="addAnimTrack()">➕ Ajouter un clip</button>
</div>
<div id="animClipForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="animClipFormTitle">Clip d'animation</strong>
<button type="button" class="smallBtn button is-small" onclick="closeAnimClipForm()">✕</button>
</div>
<label>Type
<select id="anim-clip-kind" onchange="toggleAnimClipKind()">
<option value="animate_css">Animation Animate.css</option>
<option value="custom">Animation personnalisée — images-clés</option>
</select>
</label>
<div id="anim-clip-animate-css-fields">
<label>Animation
<select id="anim-clip-animate-name">
{% for category, items in animate_css_catalog %}
<optgroup label="{{ category }}">
{% for key, label in items %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</label>
</div>
<div id="anim-clip-custom-fields" style="display:none;">
<div id="anim-clip-keyframes"></div>
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
</div>
<div class="fieldRow">
<label style="flex:1;">Départ en secondes
<input type="number" id="anim-clip-start" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Durée en secondes
<input type="number" id="anim-clip-duration" min="0.1" step="0.1" value="1">
</label>
</div>
<div class="fieldRow">
<label style="flex:1;">Délai en secondes
<input type="number" id="anim-clip-delay" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Vitesse
<select id="anim-clip-easing">
<option value="ease">Progressive</option>
<option value="linear">Constante</option>
<option value="ease-in">Accélère</option>
<option value="ease-out">Ralentit</option>
<option value="ease-in-out">Accélère puis ralentit</option>
</select>
</label>
</div>
<label>Répétitions
<select id="anim-clip-iteration">
<option value="1">Une fois</option>
<option value="2">2 fois</option>
<option value="3">3 fois</option>
<option value="infinite">En boucle</option>
</select>
</label>
<div class="actionsRow">
<button type="button" class="smallBtn button is-small is-primary" onclick="saveAnimClipForm()">Enregistrer</button>
<button type="button" class="smallBtn button is-small" onclick="previewAnimClip()">▶ Aperçu</button>
<button type="button" class="smallBtn button is-small is-danger" onclick="deleteAnimClipForm()">🗑️ Supprimer</button>
</div>
</div>
<div class="animTimelineWrap">
<div id="animRuler" class="animRuler"></div>
<div id="animTracks" class="animTracks"></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)">
<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>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>
{% 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>
<!-- Panneau droit : propriétés de l'élément sélectionné — flottant,
déplaçable à la souris par son en-tête, fermable pour agrandir le
canevas (voir floatPanel*() plus bas). -->
<button type="button" id="propsPanelReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openPropsPanel()" title="Rouvrir le panneau de propriétés">⚙️ Propriétés</button>
<div class="floatPanel floatPanel--right" id="propsPanelFloat">
<div class="floatPanelHeader" data-drag-panel="propsPanelFloat" data-drag-key="props">
<span class="floatPanelTitle">⋮⋮ Propriétés</span>
<button type="button" class="button is-small" onclick="closePropsPanel()" title="Fermer">✕</button>
</div>
<div class="floatPanelBody">
<!-- Sélecteur visuel d'icône, ouvert depuis le réglage "Icône" d'un
élément Icône déjà posé (voir plus bas dans la boucle des contrôles)
— même galerie que ci-dessus, mais choisir une icône ici REMPLACE
celle de l'élément sélectionné au lieu d'en ajouter un nouveau. Une
seule instance partagée (pas besoin de la refaire par élément). -->
<div class="modal" id="iconPickerModal">
<div class="modal-background" onclick="closeIconPicker()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Choisir une icône</p>
<button class="delete" aria-label="Fermer" onclick="closeIconPicker()"></button>
</header>
<section class="modal-card-body">
<input type="text" class="input is-small" placeholder="Rechercher une icône..." oninput="filterIconPickerModal(this.value)">
<div class="iconGallery" id="iconPickerGrid" style="max-height:50vh;">
{% for slug in fontawesome_icons %}
<button type="button" class="iconGalleryTile" data-icon-search="{{ slug }}"
title="{{ slug }}" onclick="chooseIcon('{{ slug }}')">
<i class="icon-svg" style="--icon-url:url('/static/icons/{{ slug }}.svg');"></i>
</button>
{% endfor %}
</div>
</section>
</div>
</div>
<!-- Confirmation de suppression d'un élément — remplace le confirm()
natif du navigateur pour pouvoir avertir, le cas échéant, que des
nœuds de la Logique de la scène seront aussi supprimés (voir
handleDeleteElementSubmit()/element_delete_impact.py) : le
confirm() natif ne pouvait afficher qu'un texte figé au moment du
rendu, jamais une info calculée après coup. -->
<div class="modal" id="deleteConfirmModal">
<div class="modal-background" onclick="closeDeleteConfirmModal()"></div>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">🗑️ <span id="deleteConfirmMessage">Supprimer cet élément ?</span></p>
<button class="delete" aria-label="Fermer" onclick="closeDeleteConfirmModal()"></button>
</header>
<section class="modal-card-body">
<p class="hint">Cette action est irréversible.</p>
<p class="hint" id="deleteConfirmFlowWarning" style="display:none; margin-top:10px; color:var(--danger, #f04438);"></p>
</section>
<footer class="modal-card-foot" style="justify-content:flex-end; gap:8px;">
<button type="button" class="button is-small" onclick="closeDeleteConfirmModal()">Annuler</button>
<button type="button" class="button is-small is-danger" onclick="confirmDeleteElement()">🗑️ Supprimer</button>
</footer>
</div>
</div>
{% if not selected_id %}
<div class="hint" style="padding:20px 0;">Sélectionne un élément (sur l'écran ou dans la liste) pour voir et modifier ses propriétés ici.</div>
{% else %}
{% set el = elements|selectattr("id", "equalto", selected_id)|first %}
{% if el %}
{% set w = widgets.get(el.widget, {}) %}
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<h2 class="subtitle is-6" style="margin:0;">{{ w.get('icon','❔') }} {{ el|elabel }}{% if el.name %} <span class="has-text-grey">— {{ w.get('label', el.widget) }}</span>{% endif %}</h2>
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=el.id) }}"
data-confirm-message="Supprimer cet élément ?" onsubmit="return handleDeleteElementSubmit(event, this);">
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=el.id) }}" class="field has-addons" style="margin:6px 0;">
<div class="control is-expanded"><input class="input" type="text" name="name" value="{{ el.name or '' }}" placeholder="Donne un nom à cet élément"></div>
<div class="control"><button type="submit" class="button is-small">Nommer</button></div>
</form>
<div class="field has-addons" style="margin:6px 0;">
<div class="control"><a class="button is-small is-static">ID</a></div>
<div class="control is-expanded"><input class="input" type="text" value="elt-{{ el.id }}" readonly style="opacity:0.7;"></div>
</div>
{% if el.widget == 'onglets' %}
{% set tabs = children_of.get(el.id, []) %}
<details class="propGroup">
<summary>🗂️ Onglets{{ ' (' ~ tabs|length ~ ')' if tabs else '' }}</summary>
<div class="propGroupBody">
{% if tabs %}
{% for tab in tabs %}
<div class="box" style="padding:10px; display:flex; flex-direction:column; gap:6px;">
<form method="post" action="{{ url_for('element_rename', slug=game.slug, element_id=tab.id) }}" class="field has-addons" style="margin:0;">
<div class="control is-expanded"><input class="input is-small" type="text" name="name" value="{{ tab.name or ('Onglet ' ~ loop.index) }}" placeholder="Nom de l'onglet"></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
<div class="field has-addons" style="margin:0;">
<div class="control is-expanded"><a class="button is-small is-fullwidth" href="{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id, selected=tab.id) }}">📥 Contenu de l'onglet</a></div>
<div class="control">
<form method="post" action="{{ url_for('element_delete', slug=game.slug, element_id=tab.id) }}"
data-confirm-message="Supprimer cet onglet et tout son contenu ?" onsubmit="return handleDeleteElementSubmit(event, this);">
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="help">Aucun onglet pour l'instant.</p>
{% endif %}
<form method="post" action="{{ url_for('element_tab_add', slug=game.slug, element_id=el.id) }}" style="margin-top:8px;">
<button type="submit" class="button is-small is-primary is-fullwidth">➕ Ajouter un onglet</button>
</form>
</div>
</details>
{% endif %}
{% set size_ctrl_keys = ['child-width', 'child-height'] %}
{% set size_ctrls = {} %}
{% for c in selected_controls if c.key in size_ctrl_keys %}{% set _ = size_ctrls.update({c.key: c}) %}{% endfor %}
<form method="post" action="{{ url_for('element_save', slug=game.slug, element_id=el.id) }}" id="propsForm" onsubmit="event.preventDefault(); return false;">
<details class="propGroup">
<summary>Position &amp; taille</summary>
<div class="propGroupBody">
<div class="columns is-multiline posGrid">
<div class="column is-half">
<div class="field"><label class="label is-small">X (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-x" min="0" max="100" value="{{ el.x }}" {{ 'disabled' if el.parent_id else '' }}></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Y (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-y" min="0" max="100" value="{{ el.y }}" {{ 'disabled' if el.parent_id else '' }}></div>
</div>
</div>
{% if el.parent_id %}
<div class="column is-half">
<div class="field"><label class="label is-small">Largeur</label>
<div class="field has-addons sizeField">
<div class="control is-expanded"><input class="input is-small" type="number" name="ctrl_child-width_value" min="0" value="{{ size_ctrls['child-width'].value.value or '' }}" placeholder="auto"></div>
<div class="control">
<div class="select is-small">
<select name="ctrl_child-width_unit">
<option value="%" {{ 'selected' if size_ctrls['child-width'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-width'].value.unit == 'px' else '' }}>px</option>
</select>
</div>
</div>
</div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Hauteur</label>
<div class="field has-addons sizeField">
<div class="control is-expanded"><input class="input is-small" type="number" name="ctrl_child-height_value" min="0" value="{{ size_ctrls['child-height'].value.value or '' }}" placeholder="auto"></div>
<div class="control">
<div class="select is-small">
<select name="ctrl_child-height_unit">
<option value="%" {{ 'selected' if size_ctrls['child-height'].value.unit == '%' else '' }}>%</option>
<option value="px" {{ 'selected' if size_ctrls['child-height'].value.unit == 'px' else '' }}>px</option>
</select>
</div>
</div>
</div>
</div>
</div>
{% else %}
<div class="column is-half">
<div class="field"><label class="label is-small">Largeur (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-w" min="1" max="100" value="{{ el.width }}"></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Hauteur (%)</label>
<div class="control"><input class="input is-small" type="number" id="pos-h" min="1" max="100" value="{{ el.height }}"></div>
</div>
</div>
{% endif %}
</div>
{% if el.parent_id %}
<p class="help">X/Y ne s'appliquent qu'à un élément posé directement sur l'écran — celui-ci suit la disposition de son conteneur. Largeur/Hauteur : laisser vide garde la taille automatique ; "%" (relatif au conteneur) convient à la plupart des cas, "px" pour une taille précise.</p>
{% endif %}
</div>
</details>
{% set ns = namespace(last_group=None, started=False) %}
{% for c in selected_controls if c.key not in size_ctrl_keys %}
{% if c.group != ns.last_group %}
{% if ns.started %}
</div>
</details>
{% endif %}
<details class="propGroup">
<summary>{{ c.group or 'Propriétés' }}</summary>
<div class="propGroupBody">
{% set ns.last_group = c.group %}
{% set ns.started = True %}
{% endif %}
<div class="field controlRow{{ ' visCondRow' if c.key.startswith('visibilite_cond_') and c.key != 'visibilite_cond_mode' else '' }}"
{% if c.key in ('visibilite_cond_variable', 'visibilite_cond_variable_chemin') %}data-vis-cond-scope="variable"
{% elif c.key in ('visibilite_cond_definition_id', 'visibilite_cond_champ') %}data-vis-cond-scope="objet"
{% elif c.key in ('visibilite_cond_operateur', 'visibilite_cond_valeur') %}data-vis-cond-scope="any"
{% endif %}>
{% if c.type in ('checkbox', 'toggle_token') %}
<div class="control">
<label class="checkbox">
<input type="checkbox" name="ctrl_{{ c.key }}" {{ 'checked' if c.value else '' }}>
{{ c.label }}
</label>
</div>
{% else %}
<label class="label is-small">{{ c.label }}</label>
{% if c.key == 'icon_class' %}
<div class="control">
<button type="button" class="button is-fullwidth" onclick="openIconPicker()">
<span class="icon"><i id="iconPickerPreview" class="icon-svg" style="--icon-url:url('/static/icons/{{ c.value or c.default }}.svg');"></i></span>
<span>Changer l'icône</span>
</button>
<input type="hidden" name="ctrl_{{ c.key }}" id="field-{{ c.key }}" value="{{ c.value or c.default }}">
</div>
{% elif c.key.endswith('_valeur') %}
<!-- Valeur de comparaison d'un filtre — soit une valeur fixe
tapée à la main, soit une donnée d'un AUTRE objet qui change
en cours de partie (ex: le niveau courant), choisie via deux
listes déroulantes plutôt que tapée à la main sous la forme
interne attendue — voir initFilterValuePickers() et
updateFilterValueFromDynamic() plus bas, qui reconstruisent
cette syntaxe en coulisses dans le champ caché ci-dessous,
seul réellement envoyé au serveur. -->
<div class="control filterValueControl" data-current-value="{{ c.value }}">
<div class="select is-small is-fullwidth">
<select class="filterValueMode" onchange="onFilterValueModeChange(this)">
<option value="fixe">Valeur fixe</option>
<option value="dynamique">Donnée d'un autre objet (change en jeu)</option>
<option value="variable">Variable globale (change en jeu)</option>
</select>
</div>
<div class="filterValueFixe" style="margin-top:6px;">
<input class="input is-small" type="text" name="ctrl_{{ c.key }}" id="field-{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}">
</div>
<div class="filterValueDynamique" style="display:none; margin-top:6px;">
<div class="select is-small is-fullwidth">
<select class="filterValueObjSel" onchange="onFilterValueObjChange(this)">
<option value="">— objet —</option>
{% for d in definitions %}
<option value="{{ d.name }}" data-definition-id="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
</div>
<div class="select is-small is-fullwidth" style="margin-top:6px;">
<select class="filterValueFieldSel" onchange="onFilterValueFieldChange(this)">
<option value="">— champ —</option>
</select>
</div>
</div>
<div class="filterValueVariable" style="display:none; margin-top:6px;">
<div class="select is-small is-fullwidth">
<select class="filterValueVarSel" onchange="onFilterValueVarChange(this)">
<option value="">— variable —</option>
{% for v in global_variables %}
<option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %}
</select>
</div>
<!-- Utile seulement si la variable choisie est de type
"objet"/"tableau" : sans chemin, c'est toute la variable
(son JSON) qui est utilisée comme valeur de comparaison. -->
<input class="input is-small filterValueVarPath" type="text" placeholder="Chemin optionnel (ex. .nom ou [0].valeur)" style="margin-top:6px;" oninput="updateFilterValueFromVariable(this.closest('.filterValueControl'))">
{% if not global_variables %}
<p class="help">Aucune variable globale pour l'instant — <a href="{{ url_for('game_dashboard', slug=game.slug, tab='variables') }}" target="_blank">crées-en une ↗</a></p>
{% endif %}
</div>
</div>
{% elif c.type in ('text', 'url') %}
<div class="control">
<input class="input {{ 'uploadTextField' if c.allow_upload else '' }}" type="text" name="ctrl_{{ c.key }}" value="{{ c.value }}" placeholder="{{ c.placeholder or '' }}" id="field-{{ c.key }}">
</div>
{% if c.allow_upload %}
<div class="control uploadRow">
<input type="file" class="uploadInput" accept="{{ c.upload_accept }}" data-target="field-{{ c.key }}">
<span class="help uploadStatus"></span>
</div>
<p class="help">Colle une adresse (URL) ci-dessus, ou choisis un fichier sur ton ordinateur — il sera hébergé automatiquement avec ce jeu.</p>
{% endif %}
{% if c.field_options %}
<div class="control fieldRow" style="margin-top:6px;">
<div class="select is-small is-expanded"><select id="fieldPicker-{{ c.key }}">
<option value="">— champ de l'objet lié —</option>
{% for fname, flabel in c.field_options %}
<option value="{{ fname }}">{{ flabel }}</option>
{% endfor %}
</select></div>
<button type="button" class="button is-small" onclick="insertFieldAtCursor('field-{{ c.key }}', 'fieldPicker-{{ c.key }}')">+ Ajouter</button>
</div>
{% endif %}
{% elif c.type == 'textarea' %}
<div class="control"><textarea class="textarea" name="ctrl_{{ c.key }}" id="field-{{ c.key }}" rows="4" placeholder="{{ c.placeholder or '' }}">{{ c.value }}</textarea></div>
{% if c.field_options %}
<div class="control fieldRow" style="margin-top:6px;">
<div class="select is-small is-expanded"><select id="fieldPicker-{{ c.key }}">
<option value="">— champ de l'objet lié —</option>
{% for fname, flabel in c.field_options %}
<option value="{{ fname }}">{{ flabel }}</option>
{% endfor %}
</select></div>
<button type="button" class="button is-small" onclick="insertFieldAtCursor('field-{{ c.key }}', 'fieldPicker-{{ c.key }}')">+ Ajouter</button>
</div>
<p class="help">Insère "&#123;&#123;champ&#125;&#125;" à l'emplacement du curseur — affichera la valeur de ce champ, sur la ligne trouvée par "Donnée liée" ci-dessous.</p>
{% endif %}
{% elif c.type == 'color' %}
<div class="control"><input class="input" type="color" name="ctrl_{{ c.key }}" value="{{ c.value or c.default }}"></div>
{% elif c.type == 'slider' %}
<div class="control sliderRow">
<input type="range" name="ctrl_{{ c.key }}" min="{{ c.min }}" max="{{ c.max }}" step="{{ c.step }}" value="{{ c.value }}"
oninput="this.nextElementSibling.textContent = this.value + '{{ c.unit }}'">
<span class="sliderValue tag">{{ c.value }}{{ c.unit }}</span>
</div>
{% elif c.type == 'scale' %}
<div class="control sliderRow">
<input type="range" name="ctrl_{{ c.key }}" min="{{ c.min }}" max="{{ c.max }}" step="{{ c.step }}" value="{{ c.value }}"
oninput="this.nextElementSibling.textContent = 'x' + this.value">
<span class="sliderValue tag">x{{ c.value }}</span>
</div>
{% elif c.type == 'clause_list' %}
<!-- Liste de conditions ILLIMITÉE (champ/opérateur/valeur),
combinées par ET/OU — voir c_clause_list.py. Chaque bloc
reprend EXACTEMENT la présentation d'avant (un "Champ" et un
"...est" clairement étiquetés, et le même sélecteur de valeur
fixe/dynamique/variable — jamais la syntaxe "&#123;&#123; &#125;&#125;" brute à
lire/taper), simplement répété autant de fois que
nécessaire. Chaque ligne existante est rendue ici (repli
côté serveur, notamment après un rechargement de page) ;
"+ Ajouter une condition" clone le <template> ci-dessous en
JS pour en ajouter d'autres à la volée (voir addClauseRow()
plus bas) — initFilterValuePickers() (déjà utilisée par
l'ancien réglage à 2 filtres fixes, entièrement scopée par
classe CSS via .closest('.filterValueControl'), jamais par
id) fonctionne donc ici aussi tel quel, avec un nombre
quelconque de blocs "valeur" sur la même page. -->
<div class="clauseListControl" data-clause-key="{{ c.key }}">
<div class="clauseListWrap" id="clauseListWrap-{{ c.key }}">
{% for clause in c.value.clauses %}
{% include "partials/clause_row.html" with context %}
{% endfor %}
</div>
<button type="button" class="button primary is-fullwidth" style="margin-top:10px;" onclick="addClauseRow('{{ c.key }}')">+ Ajouter une condition</button>
<div class="field clauseCombinatorRow" id="clauseCombinatorRow-{{ c.key }}" style="margin-top:12px; {{ '' if c.value.clauses|length > 1 else 'display:none;' }}">
<label class="label is-small">Combiner ces conditions avec</label>
<div class="control">
<div class="select is-small is-fullwidth">
<select name="ctrl_{{ c.key }}_combinateur">
<option value="et" {{ 'selected' if c.value.combinateur != 'ou' else '' }}>ET — toutes les conditions doivent être vraies</option>
<option value="ou" {{ 'selected' if c.value.combinateur == 'ou' else '' }}>OU — au moins une condition suffit</option>
</select>
</div>
</div>
</div>
<p class="help">Aucune condition = la ligne la plus récente de cet objet.</p>
</div>
{% set clause = {"champ": "", "operateur": "egal", "valeur": ""} %}
<template id="clauseRowTemplate-{{ c.key }}">
{% include "partials/clause_row.html" with context %}
</template>
{% elif c.type == 'shadow' %}
<div class="control">
<div class="fieldRow">
<div class="formField" style="flex:1;"><label class="label is-small">Décalage X</label><input class="input is-small" type="number" name="ctrl_{{ c.key }}_x" value="{{ c.value.x }}"></div>
<div class="formField" style="flex:1;"><label class="label is-small">Décalage Y</label><input class="input is-small" type="number" name="ctrl_{{ c.key }}_y" value="{{ c.value.y }}"></div>
</div>
<div class="fieldRow" style="margin-top:6px;">
<div class="formField" style="flex:1;"><label class="label is-small">Flou</label><input class="input is-small" type="number" min="0" name="ctrl_{{ c.key }}_blur" value="{{ c.value.blur }}"></div>
<div class="formField" style="flex:1;"><label class="label is-small">Étendue</label><input class="input is-small" type="number" min="0" name="ctrl_{{ c.key }}_spread" value="{{ c.value.spread }}"></div>
</div>
<div class="fieldRow" style="margin-top:6px; align-items:center;">
<input class="input" type="color" style="flex:0 0 60px; padding:2px;" name="ctrl_{{ c.key }}_color" value="{{ c.value.color }}">
<div class="sliderRow" style="flex:1;">
<input type="range" name="ctrl_{{ c.key }}_opacity" min="0" max="1" step="0.05" value="{{ c.value.opacity }}"
oninput="this.nextElementSibling.textContent = Math.round(this.value * 100) + '%'">
<span class="sliderValue tag">{{ (c.value.opacity * 100)|round|int }}%</span>
</div>
</div>
<p class="help">Décalages, flou et étendue tous à 0 = pas d'ombre.</p>
</div>
{% elif c.type == 'align' %}
<div class="control">
<div class="buttons has-addons alignGroup">
{% for val, icon in [('left','⬅️'),('center','⬛'),('right','➡️'),('justify','☰')] %}
<label class="button is-small alignOption {{ 'is-primary active' if c.value == val else '' }}">
<input type="radio" name="ctrl_{{ c.key }}" value="{{ val }}" {{ 'checked' if c.value == val else '' }} onchange="syncAlign(this)" style="display:none;">
{{ icon }}
</label>
{% endfor %}
</div>
</div>
{% elif c.type in ('select', 'preset') %}
<div class="control">
<div class="select is-fullwidth">
<select name="ctrl_{{ c.key }}" id="field-{{ c.key }}" {% if c.key == 'visibilite_cond_mode' %}onchange="toggleVisCondFields(this)"{% endif %}>
{% for opt in c.options %}
<option value="{{ opt[0] }}" {{ 'selected' if c.value == opt[0] else '' }}>{{ opt[1] }}</option>
{% endfor %}
</select>
</div>
</div>
{% if c.key == 'visibilite_cond_variable' %}
<p class="help">Pas la variable qu'il te faut ? <a href="{{ url_for('game_dashboard', slug=game.slug, tab='variables') }}" target="_blank">Gérer les variables du jeu ↗</a></p>
{% endif %}
{% endif %}
{% endif %}
</div>
{% endfor %}
{% if ns.started %}
</div>
</details>
{% endif %}
<div class="actionsRow"><span class="hint autosaveStatus" id="autosaveStatus">Enregistré automatiquement</span></div>
</form>
{% endif %}
{% endif %}
</div>
</div>
</div>
<script>
// Données injectées par Jinja (côté serveur) — tout le reste de l'éditeur
// vit maintenant dans des fichiers statiques sous static/js/screen_edit/
// (voir plan de modularisation : un fichier = une responsabilité, aussi
// pour le JS — même patron déjà en place côté moteur de jeu, voir
// static/js/play/), qui ne peuvent pas contenir de code Jinja puisqu'ils
// ne passent jamais par le moteur de templates.
var GAME_SLUG = "{{ game.slug }}";
var SCREEN_ID = {{ screen.id }};
var SCREEN_EDIT_URL = "{{ url_for('screen_edit', slug=game.slug, screen_id=screen.id) }}";
var ELEMENT_ADD_URL = "{{ url_for('element_add', slug=game.slug, screen_id=screen.id) }}";
var CURRENT_SELECTED_ID = {{ selected_id or 'null' }};
var DEFINITIONS_DATA = {{ definitions_json|safe }};
var DEFINITIONS_NAMES = {{ definitions_names_json|safe }};
var FLOW_NODES_INITIAL = {{ flow_nodes_json|safe }};
var FLOW_EDGES_INITIAL = {{ flow_edges_json|safe }};
var FLOW_BLOCKS = {{ flow_blocks_json|safe }};
var ELEMENTS_LABELS = {{ elements_labels_json|safe }};
var SCREENS_MAP = {{ screens_map_json|safe }};
var ACTION_TYPE_LABELS_MAP = {{ action_type_labels_json|safe }};
var ELEMENT_PROPERTY_LABELS_MAP = {{ element_action_property_labels_json|safe }};
var ELEMENT_VISIBILITY_LABELS_MAP = {{ element_visibility_labels_json|safe }};
var ELEMENT_SURBRILLANCE_LABELS_MAP = {{ element_surbrillance_labels_json|safe }};
var ELEMENT_DESACTIVE_LABELS_MAP = {{ element_desactive_labels_json|safe }};
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
var TRIGGER_LABELS_MAP = {{ trigger_labels_json|safe }};
var CUSTOM_EVENTS_MAP = {{ custom_events_map_json|safe }};
var ANIM_CLIPS = {{ animation_clips_json|safe }};
var ANIMATE_CSS_LABELS = {{ animate_css_labels_json|safe }};
</script>
<script src="{{ url_for('static', filename='js/screen_edit/tree-panels.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/panel-init.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/flow-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/tabs-and-blocks.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/animation-timeline.js') }}"></script>
<script>
// Onglet initial : ?tab=... (utilisé par les routes de CRUD des
// événements/blocs, pour revenir directement sur le bon onglet après
// création/modification/suppression plutôt que de laisser deviner où
// cliquer — même patron que game_dashboard.html). ?block=<id> ouvre en
// plus directement le bloc concerné (même idiome que ?edit=&subtab= dans
// game_dashboard.html). Reste inline (pas dans un fichier statique) :
// dépend directement de request.args, données Jinja propres à cette
// requête, et doit s'exécuter APRÈS les fichiers ci-dessus (switchBuilderTab/
// openLogicBlockPanel doivent déjà être définis).
(function () {
var requestedTab = {{ (request.args.get('tab') or '')|tojson }};
if (requestedTab) switchBuilderTab(requestedTab);
var requestedBlock = {{ request.args.get('block', type=int)|tojson }};
if (requestedBlock) openLogicBlockPanel(requestedBlock);
})();
</script>
{% endblock %}