Files
Forge-Engine/templates/scene_edit.html
T
williamandClaude Sonnet 5 51427b5297
Build and deploy / test-python (push) Successful in 6m14s
Build and deploy / test-js (push) Successful in 51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Éditeur de quêtes : titre/objectif/récompense/statut/résultat + dialogue à 3 colonnes
Nouvel espace "🗺️ Quêtes" (game-wide, comme les événements
personnalisés) : liste de cartes + "+ Nouvelle quête", chaque carte
ouvrant une modale d'arbre de dialogue en bulles de théâtre
("speaker: texte"), une colonne par statut de quête (nouvelle/en
cours/terminée) puisque le dialogue joué dépend de l'état de la
quête au moment où le joueur parle au PNJ. Bulles alternées
joueur/PNJ, couleur différente selon qui parle.

L'action "Déclencher une quête" de l'éditeur de collision n'est plus
un champ texte libre : elle ouvre directement CETTE MÊME modale
(picker de quêtes existantes + création à la volée), et finalise la
règle avec l'id numérique de la quête choisie — collision_rules.py
migré en conséquence (quete_id devient un entier, comme evenement_id).

Corrige aussi la miniature d'objet de la carte "🧩 Collision", 3× plus
grande (120px) comme demandé.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-02 20:24:57 +02:00

460 lines
28 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') }}">
<style>
/* render_scene_object.py positionne son <img> en absolu (left/top/width/
height en px) pour s'afficher correctement en enfant DIRECT de
.playScreen.playScene (voir templates/play.html) — dans le canevas de
CET éditeur, cette même balise est nichée dans .canvasElementInner,
lui-même déjà positionné/dimensionné par .canvasElement (voir plus
bas) : sans ce correctif, l'image se positionnait EN PLUS depuis
#canvas (son ancêtre positionné le plus proche), en double du
décalage déjà appliqué par .canvasElement. */
.canvasElementInner > img{ position:static !important; left:auto !important; top:auto !important; width:100% !important; height:100% !important; z-index:auto !important; }
</style>
{% endblock %}
{% block content %}
<!-- Fondations multi-éditeurs — éditeur de scène 2D (jeux "jeu_2d", voir
db/games/get_game_type.py). Le graphe de logique générique (blocs de
logique/Timeline d'animation, voir templates/screen_edit.html, gardé
tel quel pour l'éditeur document) a été retiré de CET éditeur à la
demande de l'utilisateur, en prévision d'un éditeur dédié aux scènes
2D (collision/périmètre -> action), pas encore construit. -->
<p class="hint" style="background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:10px 14px; margin:10px 0;">
🎮 Jeu 2D — scène à taille fixe ({{ screen.scene_width }}×{{ screen.scene_height }} px).
</p>
<div class="builder3" id="builder3">
{% set obj = objects|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
<button type="button" id="panelLeftReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openLeftPanel()" title="Rouvrir le panneau des objets">🧩 Objets</button>
<div class="floatPanel floatPanel--left" id="panelLeftFloat">
<div class="floatPanelHeader" data-drag-panel="panelLeftFloat" data-drag-key="left">
<span class="floatPanelTitle">⋮⋮ Objets de cette scène</span>
<button type="button" class="button is-small" onclick="closeLeftPanel()" title="Fermer">✕</button>
</div>
<div class="floatPanelBody">
<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>
<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>
<hr style="border-color:var(--border); margin:10px 0;">
<!-- "Objets de cette scène" (arborescence des objets déjà posés)
retirée à la demande de l'utilisateur — un objet posé reste
sélectionnable en cliquant directement dessus sur le canevas
(voir onSceneObjectMouseDown, static/js/scenes/scene-editor.js). -->
<!-- Galerie de personnages Forge (VRAIES miniatures) — cliquer une
tuile pose un personnage déjà configuré, animé immédiatement (voir
personnage-preview.js, réutilisé tel quel : ne dépend que de
data-personnage, pas de la table d'origine). -->
<details class="propGroup" open>
<summary>🎭 Personnages</summary>
<div class="propGroupBody">
<div class="iconGallery personnageGallery">
{% for slug_c, character in sprite_library_families %}
<button type="button" class="iconGalleryTile" title="{{ character.label }}" onclick="addSceneObject('personnage', '{{ slug_c }}')">
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
</button>
{% endfor %}
</div>
<p class="help">Personnage Kenney (licence libre CC0) ou animal Forge, avec toutes ses animations — les variantes de couleur d'un animal se choisissent depuis le panneau de propriétés.</p>
</div>
</details>
{% if background_library %}
<!-- Images de fond CraftPix (screens.BACKGROUND_LIBRARY, admin
seulement — voir core/sprite_gate.py). Posée à la taille RÉELLE
de l'image (voir screens/scenes/add_scene_object.py) : plus
grande que la scène, la caméra la suit en défilant derrière le
personnage (voir static/js/play/personnage-controller.js). -->
<details class="propGroup">
<summary>🏞️ Images de fond</summary>
<div class="propGroupBody">
<div class="iconGallery personnageGallery">
{% for slug_b, background in background_library.items() %}
<button type="button" class="iconGalleryTile" title="{{ background.label }}" onclick="addSceneObject('fond', null, '{{ slug_b }}')">
<img src="{{ background.url }}" alt="{{ background.label }}" class="personnageThumb">
</button>
{% endfor %}
</div>
<p class="help">Posée à sa taille réelle (souvent plus grande que la scène) — un seul fond par scène recommandé, la caméra le suit en défilant.</p>
</div>
</details>
{% endif %}
</div>
</div>
<div class="builderCanvasArea">
<div class="builderTabs" id="builderTabs">
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Scène</button>
<button type="button" class="builderTabBtn" data-tab="collision-rules" onclick="switchBuilderTab('collision-rules')">🧩 Collision</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="width:{{ screen.scene_width }}px; height:{{ screen.scene_height }}px; aspect-ratio:auto; margin:0 auto; position:relative; overflow:hidden; background:#1a1d24;">
{% for o in objects %}
<div class="canvasElement {{ 'selected' if o.id == selected_id else '' }}" id="el-{{ o.id }}"
style="position:absolute; left:{{ o.x }}px; top:{{ o.y }}px; width:{{ o.width }}px; height:{{ o.height }}px;"
onmousedown="onSceneObjectMouseDown(event, {{ o.id }})">
<div class="canvasElementInner">{{ o.rendered_html|safe }}</div>
<div class="resizeHandle" onmousedown="onSceneObjectResizeMouseDown(event, {{ o.id }})"></div>
{% if o.id == selected_id and o.kind != 'fond' %}
<!-- Boîte de collision (voir screens/rendering/collision_settings.py) :
aperçu visuel + réglable directement au glisser — seulement pour
l'objet SÉLECTIONNÉ (celui dont le panneau "🧱 Collision" est ouvert),
voir initCollisionBoxOverlay() dans scene-editor.js. JAMAIS pour un
"fond" (voir la même garde sur le panneau "🧱 Collision" plus bas). -->
<div class="collisionBoxOverlay {{ 'is-circle' if collision_settings.shape == 'cercle' else '' }} {{ 'is-disabled' if not collision_settings.enabled else '' }}"
id="collisionBoxOverlay"
style="left:{{ (o.width - collision_settings.width) / 2 + collision_settings.offset_x }}px; top:{{ (o.height - collision_settings.height) / 2 + collision_settings.offset_y }}px; width:{{ collision_settings.width }}px; height:{{ collision_settings.height }}px;"
onmousedown="onCollisionBoxMouseDown(event, {{ o.id }})">
<div class="collisionResizeHandle" onmousedown="onCollisionBoxResizeMouseDown(event, {{ o.id }})"></div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<p class="hint" style="text-align:center;">Glisse pour déplacer, tire le coin pour redimensionner, clique pour sélectionner. Positions en pixels (scène à taille fixe).</p>
</div>
<!-- Éditeur de collision (voir screens/rendering/collision_rules.py) :
une carte par objet ÉLIGIBLE (jamais un "fond", jamais le
personnage "joueur" — ce sont toujours les AUTRES objets qui
réagissent à SA présence), listant ses règles "déclencheur ->
action" déjà posées + un bouton "+ Action" qui ouvre l'assistant
(modale plus bas). Rendu ENTIÈREMENT en JS (voir
static/js/scenes/collision-rules-editor.js) à partir de
COLLISION_RULE_OBJECTS/CUSTOM_EVENTS_MAP — jamais en Jinja, pour
pouvoir se redessiner à chaud après chaque ajout/suppression de
règle sans recharger la page. -->
<div class="builderTabPanel" id="collisionRulesTabPanel" data-tab-panel="collision-rules">
<div class="dashPanelHeader">
<h2>🧩 Collision</h2>
</div>
<p class="hint">Pour chaque objet (jamais le joueur lui-même), choisis ce qui doit se passer à son contact — la carte se construit au fil de tes choix, comme des pièces qui s'emboîtent.</p>
<div id="collisionRulesCards" class="collisionRulesCards"></div>
</div>
<!-- Assistant "+ Action" — un seul écran affiché à la fois
(#collisionRuleModalBody, reconstruit à chaque étape), jamais un
vrai formulaire à plusieurs pages : chaque clic choisit une
option et avance directement à la suivante. -->
<div class="modal" id="collisionRuleModal">
<div class="modal-background" onclick="closeCollisionRuleModal()"></div>
<div class="modal-content">
<div class="box">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<h2 class="title is-5" style="margin:0;">Nouvelle règle</h2>
<button type="button" class="button is-small" onclick="closeCollisionRuleModal()">✕</button>
</div>
<div id="collisionRuleModalBody"></div>
</div>
</div>
</div>
<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.</p>
{% endif %}
</div>
</div>
<!-- Panneau droit : propriétés de l'objet sélectionné. -->
<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">
{% if not selected_id or not obj %}
<div class="hint" style="padding:20px 0;">Sélectionne un objet (sur la scène ou dans la liste) pour voir et modifier ses propriétés ici.</div>
{% else %}
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<h2 class="subtitle is-6" style="margin:0;">{{ '🏃' if obj.kind == 'personnage' else '🖼️' }} {{ obj.name or ('Personnage' if obj.kind == 'personnage' else 'Décor') }}</h2>
<form method="post" action="{{ url_for('scene_object_delete', slug=game.slug, object_id=obj.id) }}" onsubmit="return confirm('Supprimer cet objet ?');">
<button type="submit" class="button is-small is-danger">🗑️</button>
</form>
</div>
<details class="propGroup" open>
<summary>Position &amp; taille (pixels)</summary>
<div class="propGroupBody">
<div class="columns is-multiline posGrid">
<div class="column is-half">
<div class="field"><label class="label is-small">X (px)</label>
<div class="control"><input class="input is-small" type="number" id="scene-pos-x" min="0" value="{{ obj.x }}"></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Y (px)</label>
<div class="control"><input class="input is-small" type="number" id="scene-pos-y" min="0" value="{{ obj.y }}"></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Largeur (px)</label>
<div class="control"><input class="input is-small" type="number" id="scene-pos-w" min="1" value="{{ obj.width }}"></div>
</div>
</div>
<div class="column is-half">
<div class="field"><label class="label is-small">Hauteur (px)</label>
<div class="control"><input class="input is-small" type="number" id="scene-pos-h" min="1" value="{{ obj.height }}"></div>
</div>
</div>
</div>
</div>
</details>
{% if obj.kind != 'fond' %}
<!-- Boîte de collision (voir screens/rendering/collision_settings.py,
static/js/play/conditions.js::elementsOverlap) : posée AUTOMATIQUEMENT
sur tout objet (= sa boîte visuelle par défaut, tant que rien
n'est changé ici) — désactivable, taille/forme/décalage réglables
pour un sprite très paddé dont la silhouette réelle est plus
petite que son canevas. JAMAIS pour un "fond" (image de décor,
voir add_scene_object.py) : ni obstacle ni cible de collision
possible (forgeSolidObstacles l'exclut déjà côté jeu, voir
static/js/play/personnage-controller.js), ce panneau n'aurait
donc aucun effet. -->
<details class="propGroup">
<summary>🧱 Collision</summary>
<div class="propGroupBody" id="collisionPanelBody" data-element-id="{{ obj.id }}">
<div class="field">
<div class="control">
<label class="checkbox">
<input type="checkbox" id="collision-enabled" {{ 'checked' if collision_settings.enabled else '' }} onchange="saveSceneObjectCollision()">
Boîte de collision activée
</label>
</div>
</div>
<div class="field"><label class="label is-small">Forme</label>
<div class="control">
<div class="select is-small">
<select id="collision-shape" onchange="saveSceneObjectCollision()">
<option value="rectangle" {{ 'selected' if collision_settings.shape == 'rectangle' else '' }}>Rectangle</option>
<option value="cercle" {{ 'selected' if collision_settings.shape == 'cercle' else '' }}>Cercle</option>
</select>
</div>
</div>
</div>
<div class="field is-grouped">
<div class="control"><label class="label is-small">Largeur (px)</label>
<input class="input is-small" type="number" min="1" id="collision-width" value="{{ collision_settings.width }}" onchange="saveSceneObjectCollision()"></div>
<div class="control"><label class="label is-small">Hauteur (px)</label>
<input class="input is-small" type="number" min="1" id="collision-height" value="{{ collision_settings.height }}" onchange="saveSceneObjectCollision()"></div>
</div>
<div class="field is-grouped">
<div class="control"><label class="label is-small">Décalage X (px)</label>
<input class="input is-small" type="number" id="collision-offset-x" value="{{ collision_settings.offset_x }}" onchange="saveSceneObjectCollision()"></div>
<div class="control"><label class="label is-small">Décalage Y (px)</label>
<input class="input is-small" type="number" id="collision-offset-y" value="{{ collision_settings.offset_y }}" onchange="saveSceneObjectCollision()"></div>
</div>
<p class="help">Taille/décalage réglés par rapport au centre de l'objet — laisse la taille par défaut (celle de l'objet) si son image n'a pas de marge inutile.</p>
</div>
</details>
{% endif %}
{% if obj.kind == 'personnage' %}
<details class="propGroup" open>
<summary>🎭 Personnage</summary>
<div class="propGroupBody" id="personnagePanelBody" data-element-id="{{ obj.id }}">
<!-- Rôle (voir screens/rendering/personnage_role.py) : seul un
personnage "Joueur" est déplacé/animé automatiquement au
clavier et suivi par la caméra — "Ennemi"/"PNJ" restent
immobiles tant qu'aucune logique de flow ne les pilote. -->
<div class="field"><label class="label is-small">🏷️ Rôle</label>
<div class="control">
<div class="select is-small">
<select id="personnage-role" onchange="saveSceneObjectRole(this.value)">
{% for value in personnage_role_choices %}
<option value="{{ value }}" {{ 'selected' if value == personnage_role else '' }}>{{ personnage_role_labels[value] }}</option>
{% endfor %}
</select>
</div>
</div>
</div>
<p class="help">Personnage Forge — clique un autre pour en changer.</p>
<div class="iconGallery personnageGallery" id="personnageGallerySwap">
{% for slug_c, character in sprite_library_families %}
<button type="button" class="iconGalleryTile {{ 'selected' if slug_c == personnage_data.forge_character else '' }}"
title="{{ character.label }}" onclick="swapSceneObjectCharacter('{{ slug_c }}')">
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
</button>
{% endfor %}
</div>
{% set current_character = sprite_library.get(personnage_data.forge_character) %}
{% if current_character and current_character.get('family') %}
<p class="help">Variantes de {{ current_character.family_label }} :</p>
<div class="iconGallery personnageGallery" id="personnageGalleryVariants">
{% for slug_c, character in sprite_library.items() if character.get('family') == current_character.family %}
<button type="button" class="iconGalleryTile {{ 'selected' if slug_c == personnage_data.forge_character else '' }}"
title="{{ character.label }}" onclick="swapSceneObjectCharacter('{{ slug_c }}')">
<img src="{{ character.animations.idle[0] }}" alt="{{ character.label }}" class="personnageThumb">
</button>
{% endfor %}
</div>
{% endif %}
</div>
</details>
<!-- Déplacement/animation automatiques (voir static/js/play/
personnage-controller.js) : ce personnage se déplace et s'anime
TOUT SEUL avec ces touches dès qu'il est posé sur la scène, sans
le moindre nœud de flow à poser — ce panneau ne fait que
permettre de les remapper/bloquer un axe. -->
<details class="propGroup">
<summary>🕹️ Commandes</summary>
<div class="propGroupBody" id="personnageCommandsBody" data-element-id="{{ obj.id }}">
<p class="help">Ce personnage se déplace et s'anime automatiquement avec ces touches, sans rien à poser dans la logique. Clique un champ puis appuie sur la touche voulue.</p>
<div class="field is-grouped">
<div class="control"><label class="label is-small">Haut</label>
<input class="input is-small personnageKeyCapture" id="cmd-haut" readonly value="{{ personnage_commands.haut }}"></div>
<div class="control"><label class="label is-small">Bas</label>
<input class="input is-small personnageKeyCapture" id="cmd-bas" readonly value="{{ personnage_commands.bas }}"></div>
</div>
<div class="field is-grouped">
<div class="control"><label class="label is-small">Gauche</label>
<input class="input is-small personnageKeyCapture" id="cmd-gauche" readonly value="{{ personnage_commands.gauche }}"></div>
<div class="control"><label class="label is-small">Droite</label>
<input class="input is-small personnageKeyCapture" id="cmd-droite" readonly value="{{ personnage_commands.droite }}"></div>
</div>
<div class="field"><label class="label is-small">Interagir</label>
<div class="control"><input class="input is-small personnageKeyCapture" id="cmd-interagir" readonly value="{{ personnage_commands.interagir }}"></div>
</div>
<div class="field"><label class="label is-small">Axe de déplacement</label>
<div class="control">
<div class="select is-small">
<select id="cmd-axe">
<option value="libre" {{ 'selected' if personnage_commands.axe == 'libre' else '' }}>Libre</option>
<option value="horizontal" {{ 'selected' if personnage_commands.axe == 'horizontal' else '' }}>Horizontal seulement (gauche/droite)</option>
<option value="vertical" {{ 'selected' if personnage_commands.axe == 'vertical' else '' }}>Vertical seulement (haut/bas)</option>
</select>
</div>
</div>
</div>
<div class="field"><label class="label is-small">Vitesse (px/tick)</label>
<div class="control"><input class="input is-small" type="number" id="cmd-vitesse" min="1" max="20" value="{{ personnage_commands.vitesse }}"></div>
</div>
<!-- Touches supplémentaires (sauter, attaquer...) : au-delà des 4
touches de déplacement et de "Interagir" ci-dessus, chacune
liée à UNE pose au choix du personnage (voir
ELEMENT_ANIMATIONS_MAP/PERSONNAGE_COMMANDS_EXTRA, déjà
connues de scene-editor.js) — construites en JS, jamais en
Jinja : le nombre de lignes change dynamiquement (+ Ajouter
/ ×). -->
<div class="field"><label class="label is-small">Animations supplémentaires (sauter, attaquer...)</label>
<div id="personnageCommandsExtra"></div>
<button type="button" class="button is-small" onclick="addPersonnageCommandExtraRow()">+ Ajouter une touche</button>
</div>
</div>
</details>
{% endif %}
{% endif %}
</div>
</div>
</div>
<script>
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 SCENE_WIDTH = {{ screen.scene_width }};
var SCENE_HEIGHT = {{ screen.scene_height }};
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 FLOW_TARGETS_OBJECTS = true;
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_ORIENTATION_LABELS_MAP = {{ element_orientation_labels_json|safe }};
var DATA_OPERATION_LABELS_MAP = {{ data_operation_labels_json|safe }};
var SCORE_STATUS_LABELS_MAP = {{ score_status_labels_json|safe }};
var CONDITION_OPERATOR_LABELS_MAP = {{ condition_operator_labels_json|safe }};
var TRIGGER_LABELS_MAP = {};
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 }};
var SPRITE_LIBRARY = {{ sprite_library_json|safe }};
var ELEMENT_ANIMATIONS_MAP = {{ element_animations_map_json|safe }};
var PERSONNAGE_COMMANDS_EXTRA = {{ (personnage_commands.extra if personnage_commands else [])|tojson }};
var COLLISION_RULE_OBJECTS = {{ collision_rule_objects_json|safe }};
var COLLISION_ACTION_TYPES = {{ collision_action_types|tojson }};
var COLLISION_TRIGGER_TYPES = {{ collision_trigger_types|tojson }};
var COLLISION_DEFAULT_PERIMETRE_PX = {{ collision_default_perimetre_px }};
// Éditeur de quêtes (voir static/js/quests/quest-editor.js) — chargé
// avant collision-rules-editor.js : l'action "quete" de l'assistant
// "+ Action" ouvre directement cet éditeur (renderQuestPickerStep).
var QUESTS_JSON = {{ quests_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/personnage-panel.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/builder-tabs.js') }}"></script>
<script src="{{ url_for('static', filename='js/screen_edit/personnage-preview.js') }}"></script>
<script src="{{ url_for('static', filename='js/scenes/scene-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/quests/quest-editor.js') }}"></script>
<script src="{{ url_for('static', filename='js/scenes/collision-rules-editor.js') }}"></script>
{% endblock %}