- Ajoute clic/survol/affichage-ecran comme declencheurs, et surbrillance, video, son, visibilite, indication, attendre comme actions, utilisables aussi bien par l'editeur manuel (menu lateral Objets/Ecran) que par Ruby (IA), avec blocs deplacables/supprimables dans une chaine. - Corrige plusieurs variantes du bug "impossible de poser un objet hors du champ de la camera" (troncature du chainage d'actions a 4 maillons, fond importe pose a 128x128 au lieu de sa taille reelle, decalage du fond au vrai glisser-depose, redimensionnement manuel jamais propage au monde). - Ajoute un vrai glisser-depose depuis la galerie vers la scene, la gestion complete de "Mes assets" (sous-sections Fonds/Decors/Sons/ Videos, suppression, reclassement fond<->decor sans re-upload). - Ajoute l'upload de son (limite 3 min) et de video (MP4 uniquement, limite 5 min), avec validation de la duree reelle du fichier, et une replique audio optionnelle dans une bulle de dialogue. - Fixe la taille de pose d'un objet/decor importe a 200x200 avec une boite de collision de 150x150. - Filtre le selecteur de fichier des actions son/video par type reel. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
785 lines
49 KiB
HTML
785 lines
49 KiB
HTML
{% 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 %}
|
||
<!-- 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. -->
|
||
<!-- "Objets"/"Propriétés" (voir panelLeftFloat/propsPanelFloat plus bas) :
|
||
posés ICI, dans la barre d'outils de la scène, plutôt qu'en position
|
||
fixe flottant au-dessus de la page (ancien "🧩 Objets"/"⚙️ Propriétés"
|
||
tout en haut de l'écran) — ils y chevauchaient "Mes écrans"/"Jouer"/
|
||
"Exporter" (voir base.html) une fois les panneaux fermés. Remplace la
|
||
simple mention "Jeu 2D — scène à taille fixe." -->
|
||
<!-- 3 zones INDÉPENDANTES (grid, jamais un flex space-between) : "Objets"/
|
||
"Propriétés" ne sont visibles que panneau fermé (display:none sinon,
|
||
voir .floatPanelReopenBtn) — avec un flex classique, la zone caméra
|
||
se retrouve seule dans la rangée la plupart du temps et se déplace
|
||
selon ce qui l'entoure. Un grid à 3 colonnes fixe sa position
|
||
(toujours centrée) sans dépendre des deux autres zones. -->
|
||
<p class="hint" style="background:var(--panel); border:1px solid var(--border); border-radius:8px; padding:10px 14px; margin:10px 0; display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:12px;">
|
||
<span><button type="button" id="panelLeftReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openLeftPanel()" title="Rouvrir le panneau des objets">Objets</button></span>
|
||
<!-- Taille de la CAMÉRA (scene_width/scene_height) — TOUJOURS
|
||
modifiable ici, directement en pixels (demande explicite : "la
|
||
caméra a la même taille [que le rectangle de prévisualisation],
|
||
c'est tout"), pas seulement suggérée automatiquement quand un
|
||
fond dépasse (voir toujours "🎥 Champ de la caméra" plus bas si
|
||
c'est le cas, purement informatif dans ce cas). -->
|
||
<span style="display:flex; align-items:center; gap:6px; justify-self:center;">
|
||
<label class="hint" style="margin:0;">Caméra (px)
|
||
<input type="number" id="scene-camera-w" class="input is-small" style="width:80px;" min="1" value="{{ screen.scene_width }}">
|
||
×
|
||
<input type="number" id="scene-camera-h" class="input is-small" style="width:80px;" min="1" value="{{ screen.scene_height }}">
|
||
</label>
|
||
<button type="button" class="button is-small" onclick="applyCameraSize()">Appliquer</button>
|
||
</span>
|
||
<span style="justify-self:end;"><button type="button" id="propsPanelReopenBtn" class="button is-small floatPanelReopenBtn" onclick="openPropsPanel()" title="Rouvrir le panneau de propriétés">Propriétés</button></span>
|
||
</p>
|
||
|
||
<div class="builder3" id="builder3">
|
||
{% set obj = objects|selectattr("id", "equalto", selected_id)|first if selected_id else none %}
|
||
|
||
<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). -->
|
||
|
||
<!-- "Mes assets" (voir auth/user_assets_dir.py) : bibliothèque PAR
|
||
COMPTE (pas par jeu) — upload manuel ou généré par l'IA (onglet
|
||
"IA", Phase 2), réutilisable d'un projet à l'autre du même
|
||
compte. Posée comme un "decor"/"fond" ordinaire (voir
|
||
screens/scenes/add_scene_object.py::image_url). PREMIER menu du
|
||
panneau (demande explicite) : le point d'entrée le plus direct
|
||
pour un créateur qui vient d'importer/générer une image. -->
|
||
<details class="propGroup" open>
|
||
<summary>Mes assets</summary>
|
||
<div class="propGroupBody">
|
||
<p class="help">Réutilisable dans tous tes projets — jamais visible par les autres comptes. Un son ne doit pas dépasser 3 minutes.</p>
|
||
|
||
<!-- Deux boutons d'import D'IMAGE distincts (demande explicite :
|
||
"un bouton d'import séparé pour les images de fond et les
|
||
objets") — le fond et le décor se posent différemment (voir
|
||
screens/scenes/add_scene_object.py : un fond démarre au coin
|
||
haut-gauche, s'empile en dessous de tout, peut devenir plus
|
||
grand que la caméra), il faut donc savoir lequel des deux le
|
||
créateur veut AVANT de poser l'objet, pas seulement au clic
|
||
sur la tuile. Le choix est mémorisé (scene_kind, voir
|
||
auth/create_user_asset.py) pour que TOUTE tuile de "Mes
|
||
assets" pose ensuite automatiquement le bon type. -->
|
||
<details class="propGroup" open>
|
||
<summary>Fonds</summary>
|
||
<div class="propGroupBody">
|
||
<label class="button is-small is-fullwidth mb-2">
|
||
+ Importer une image de fond
|
||
<input type="file" accept="image/*" style="display:none" onchange="uploadUserAsset(this, 'fond')">
|
||
</label>
|
||
<div class="iconGallery personnageGallery">
|
||
{% for asset in user_assets_fond %}
|
||
<div class="userAssetTileWrap">
|
||
<button type="button" class="iconGalleryTile" title="{{ asset.original_name or 'Asset' }}"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'fond', null, null, {{ asset.id }})"
|
||
onclick="addSceneObject('fond', null, null, {{ asset.id }})">
|
||
<img src="{{ asset.url }}" alt="{{ asset.original_name or 'Asset' }}" class="personnageThumb">
|
||
</button>
|
||
<div class="userAssetRowFooter">
|
||
<button type="button" class="userAssetReclassifyBtn" title="Utiliser comme décor/objet"
|
||
onclick="reclassifyUserAsset({{ asset.id }}, 'decor')">→ Décor</button>
|
||
<button type="button" class="userAssetDeleteBtn" title="Supprimer"
|
||
onclick="deleteUserAsset({{ asset.id }}, '{{ (asset.original_name or asset.filename)|e }}')">✕</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if not user_assets_fond %}<p class="help">Aucune image de fond importée pour l'instant.</p>{% endif %}
|
||
</div>
|
||
</details>
|
||
|
||
<details class="propGroup" open>
|
||
<summary>Décors/objets</summary>
|
||
<div class="propGroupBody">
|
||
<label class="button is-small is-fullwidth mb-2">
|
||
+ Importer un décor/objet
|
||
<input type="file" accept="image/*" style="display:none" onchange="uploadUserAsset(this, 'decor')">
|
||
</label>
|
||
<div class="iconGallery personnageGallery">
|
||
{% for asset in user_assets_decor %}
|
||
<div class="userAssetTileWrap">
|
||
<button type="button" class="iconGalleryTile" title="{{ asset.original_name or 'Asset' }}"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'decor', null, null, {{ asset.id }})"
|
||
onclick="addSceneObject('decor', null, null, {{ asset.id }})">
|
||
<img src="{{ asset.url }}" alt="{{ asset.original_name or 'Asset' }}" class="personnageThumb">
|
||
</button>
|
||
<div class="userAssetRowFooter">
|
||
<button type="button" class="userAssetReclassifyBtn" title="Utiliser comme fond"
|
||
onclick="reclassifyUserAsset({{ asset.id }}, 'fond')">→ Fond</button>
|
||
<button type="button" class="userAssetDeleteBtn" title="Supprimer"
|
||
onclick="deleteUserAsset({{ asset.id }}, '{{ (asset.original_name or asset.filename)|e }}')">✕</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% if not user_assets_decor %}<p class="help">Aucun décor/objet importé pour l'instant.</p>{% endif %}
|
||
</div>
|
||
</details>
|
||
|
||
<details class="propGroup">
|
||
<summary>Sons</summary>
|
||
<div class="propGroupBody">
|
||
<label class="button is-small is-fullwidth mb-2">
|
||
+ Importer un son
|
||
<input type="file" accept="audio/*" style="display:none" onchange="uploadUserAsset(this)">
|
||
</label>
|
||
{% for asset in user_assets_audio %}
|
||
<div class="userAssetRow">
|
||
<audio controls preload="none" src="{{ asset.url }}"></audio>
|
||
<div class="userAssetRowFooter">
|
||
<span class="userAssetName" title="{{ asset.original_name or asset.filename }}">{{ asset.original_name or asset.filename }}</span>
|
||
<button type="button" class="userAssetDeleteBtn" title="Supprimer"
|
||
onclick="deleteUserAsset({{ asset.id }}, '{{ (asset.original_name or asset.filename)|e }}')">✕</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% if not user_assets_audio %}<p class="help">Aucun son importé pour l'instant.</p>{% endif %}
|
||
<p class="help">Utilisable dans les actions "Jouer un son" des déclencheurs.</p>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="propGroup">
|
||
<summary>Vidéos</summary>
|
||
<div class="propGroupBody">
|
||
<label class="button is-small is-fullwidth mb-2">
|
||
+ Importer une vidéo (MP4, 5 min max)
|
||
<input type="file" accept="video/mp4,.mp4" style="display:none" onchange="uploadUserAsset(this)">
|
||
</label>
|
||
{% for asset in user_assets_video %}
|
||
<div class="userAssetRow">
|
||
<video controls preload="none" src="{{ asset.url }}" style="max-height:120px;"></video>
|
||
<div class="userAssetRowFooter">
|
||
<span class="userAssetName" title="{{ asset.original_name or asset.filename }}">{{ asset.original_name or asset.filename }}</span>
|
||
<button type="button" class="userAssetDeleteBtn" title="Supprimer"
|
||
onclick="deleteUserAsset({{ asset.id }}, '{{ (asset.original_name or asset.filename)|e }}')">✕</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% if not user_assets_video %}<p class="help">Aucune vidéo importée pour l'instant.</p>{% endif %}
|
||
<p class="help">Utilisable dans les actions "Jouer une vidéo" des déclencheurs.</p>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
<!-- 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">
|
||
<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 }}"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'personnage', '{{ slug_c }}', null, null)"
|
||
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 }}"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'fond', null, '{{ slug_b }}', null)"
|
||
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 %}
|
||
<!-- "🖥️ Interface" — l'UI du jeu (voir screens/rendering/
|
||
dialogue_box_style.py) : pour l'instant un seul widget, "💬 Boîte
|
||
de dialogue", gérée AUTOMATIQUEMENT par le moteur (affiche la
|
||
réplique en cours de la quête déclenchée par une règle de
|
||
collision "quete", voir static/js/play/dialogue-box-controller.js) —
|
||
l'auteur ne fait que la poser où il veut et la styliser (panneau
|
||
de propriétés de droite, une fois sélectionnée). -->
|
||
<details class="propGroup">
|
||
<summary>Interface</summary>
|
||
<div class="propGroupBody">
|
||
<button type="button" class="button is-small is-fullwidth mb-2"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'dialogue_box', null, null, null)"
|
||
onclick="addSceneObject('dialogue_box')">Boîte de dialogue</button>
|
||
<button type="button" class="button is-small is-fullwidth mb-2"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'quiz_box', null, null, null)"
|
||
onclick="addSceneObject('quiz_box')">Boîte à quiz</button>
|
||
<button type="button" class="button is-small is-fullwidth"
|
||
draggable="true" ondragstart="onGalleryTileDragStart(event, 'score_widget', null, null, null)"
|
||
onclick="addSceneObject('score_widget')">Score</button>
|
||
<p class="help">Boîte de dialogue/quiz : gérées automatiquement par le moteur (voir l'onglet "Déclencheurs") — position et style réglables une fois posées. Score : affiche en continu les points gagnés.</p>
|
||
</div>
|
||
</details>
|
||
</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="triggers" onclick="switchBuilderTab('triggers')">Déclencheurs</button>
|
||
<button type="button" class="builderTabBtn" data-tab="variables" onclick="switchBuilderTab('variables')">Variables</button>
|
||
<button type="button" class="builderTabBtn" data-tab="ia" onclick="switchBuilderTab('ia')">Générer avec l'IA</button>
|
||
{% if game.onboarding_type != 'rpg' %}
|
||
<!-- Événements personnalisés (voir db.list_custom_events) : aucun
|
||
moyen de les DÉCLENCHER depuis un jeu RPG pour l'instant — le
|
||
seul point d'entrée qui le permettait (l'action "evenement" de
|
||
l'ancien éditeur de collision) a été retiré (demande explicite,
|
||
ne servait à rien). game.onboarding_type est de toute façon
|
||
TOUJOURS "rpg" désormais (voir la suppression des types Quiz/
|
||
Embranchement) — cet onglet ne s'affiche donc plus jamais. -->
|
||
<button type="button" class="builderTabBtn" data-tab="events" onclick="switchBuilderTab('events')">Événements</button>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="builderTabPanel active" data-tab-panel="screen">
|
||
<div class="canvasFrame">
|
||
<div class="canvas" id="canvas" style="width:{{ world_width }}px; height:{{ world_height }}px; aspect-ratio:auto; margin:0 auto; position:relative; overflow:hidden; background:#1a1d24;"
|
||
ondragover="onCanvasDragOver(event)" ondrop="onCanvasDrop(event)">
|
||
{% if world_width > screen.scene_width or world_height > screen.scene_height %}
|
||
<!-- "🎥 Champ de la caméra" : le monde (ce canevas, voir
|
||
world_width/world_height ci-dessus, routes/scenes/
|
||
scene_edit_view.py) peut être plus grand que la scène
|
||
nominale dès qu'un "fond" plus grand qu'elle est posé — ce
|
||
cadre en pointillés marque où s'arrête ce que la caméra
|
||
montre RÉELLEMENT en jeu (voir static/js/play/personnage-
|
||
controller.js::forgeUpdateSceneCamera), pour que le monde
|
||
agrandi reste lisible plutôt qu'une simple étendue grise
|
||
sans repère. Purement visuel (pointer-events:none) : ne
|
||
gêne jamais la sélection/le glisser d'un objet dessous. -->
|
||
<div class="sceneCameraFrame" style="width:{{ screen.scene_width }}px; height:{{ screen.scene_height }}px;" title="Champ de la caméra en jeu ({{ screen.scene_width }}×{{ screen.scene_height }} px)"></div>
|
||
{% endif %}
|
||
{% 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 not in ('fond', 'dialogue_box', 'quiz_box', 'score_widget') %}
|
||
<!-- 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>
|
||
|
||
<!-- Onglet "Déclencheurs" (voir static/js/triggers/trigger-editor.js) :
|
||
un graphe de logique lu par déclencheur — TOUS les déclencheurs du
|
||
jeu, plus aucune notion de quête pour les regrouper (un créateur
|
||
peut faire un simple quiz sans jamais poser de personnage). Chaque
|
||
déclencheur (objet -> collision -> action) porte SON PROPRE
|
||
dialogue, édité en ligne (voir openTriggerWizard(), static/js/
|
||
scenes/collision-rules-editor.js). -->
|
||
<div class="builderTabPanel" id="triggersTabPanel" data-tab-panel="triggers">
|
||
<div id="triggersTabPanelBody"></div>
|
||
</div>
|
||
|
||
<!-- Variables globales (voir db/global_vars/) : accessibles à TOUT le
|
||
jeu par leur NOM (immuable après création — condition de
|
||
visibilité, action "Modifier une variable", voir
|
||
static/js/play/conditions.js::readVariableValue), pas seulement à
|
||
un écran. Même patron que l'onglet Événements ci-dessous (table +
|
||
formulaires classiques, POST/redirect — pas de rendu JS). -->
|
||
<div class="builderTabPanel" id="variablesTabPanel" data-tab-panel="variables">
|
||
<div class="dashPanelHeader">
|
||
<h2>Variables</h2>
|
||
<button type="button" class="button is-small primary" onclick="toggleDashCreate('variables')">+ Créer une variable</button>
|
||
</div>
|
||
<p class="hint">Accessibles depuis n'importe quelle scène (conditions, actions "Modifier une variable"). Le nom ne peut plus changer une fois créé.</p>
|
||
<form class="dashCreateBar" id="dashCreate-variables" style="display:none;" onsubmit="return createGlobalVariable(event);">
|
||
<input type="text" name="name" placeholder="NOM (ex. score_total)" required autocomplete="off">
|
||
<select name="type">
|
||
{% for key, t in global_variable_types.items() %}
|
||
<option value="{{ key }}">{{ t.label }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
<input type="text" name="default_value" placeholder="Valeur par défaut">
|
||
<label class="checkbox" style="display:flex; align-items:center; gap:4px; white-space:nowrap;" title="Chaque joueur a sa propre valeur (décoché = valeur unique partagée par tous)">
|
||
<input type="checkbox" name="per_player" value="1" checked> Par joueur
|
||
</label>
|
||
<button type="submit" class="primary">Créer</button>
|
||
</form>
|
||
|
||
<div class="fieldsTableWrap">
|
||
<table class="table is-fullwidth is-hoverable fieldsTable">
|
||
<thead><tr><th>Nom</th><th>Type</th><th>Valeur</th><th>Par joueur</th><th></th></tr></thead>
|
||
<tbody id="globalVariablesBody">
|
||
{% for v in global_variables %}
|
||
{% include "_global_variable_row.html" %}
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<p class="hint" id="globalVariablesEmptyHint" {{ 'style=display:none;' if global_variables else '' }}>Aucune variable pour l'instant — crée la première ci-dessus.</p>
|
||
</div>
|
||
|
||
<!-- Onglet "🤖 IA" (voir plan Phase 1) : scopé à CET écran uniquement
|
||
(pas tout le jeu) — le chat pilote un agent qui n'utilise que les
|
||
mêmes fonctions que l'éditeur manuel (voir ai/tools.py, Phase 2),
|
||
pour que tout ce qu'il pose reste relisable/modifiable ici. Rendu
|
||
par static/js/ia/ia-chat.js dans #iaTabPanelBody. -->
|
||
<div class="builderTabPanel" id="iaTabPanel" data-tab-panel="ia">
|
||
<div id="iaTabPanelBody" data-screen-id="{{ screen.id }}"></div>
|
||
</div>
|
||
|
||
{% if game.onboarding_type != 'rpg' %}
|
||
<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' }}">{{ 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>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Panneau droit : propriétés de l'objet sélectionné. Son bouton de
|
||
réouverture est maintenant dans la barre d'outils tout en haut
|
||
(voir #propsPanelReopenBtn plus haut), pas ici. -->
|
||
<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 %}
|
||
{% set _WIDGET_ICONS = {'dialogue_box': '💬', 'quiz_box': '❓', 'score_widget': '🏆'} %}
|
||
{% set _WIDGET_LABELS = {'dialogue_box': 'Boîte de dialogue', 'quiz_box': 'Boîte à quiz', 'score_widget': 'Score'} %}
|
||
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
|
||
<h2 class="subtitle is-6" style="margin:0;">{{ obj.name or ('Personnage' if obj.kind == 'personnage' else _WIDGET_LABELS.get(obj.kind, 'Décor')) }}</h2>
|
||
<button type="button" class="button is-small is-danger" onclick="if (confirm('Supprimer cet objet ?')) deleteSceneObject({{ obj.id }});">🗑️</button>
|
||
</div>
|
||
|
||
{% if obj.kind != 'dialogue_box' %}
|
||
<!-- "ℹ️ Informations" : le nom donné à L'OBJET (voir
|
||
screens/scenes/set_scene_object_name.py) — n'importe quel kind
|
||
(personnage, décor, fond), pas seulement un personnage — c'est
|
||
CE nom qui est ensuite proposé comme "qui parle" dans l'éditeur
|
||
de dialogue d'un déclencheur (voir "Déclencheurs", static/js/
|
||
triggers/trigger-editor.js) : un dialogue peut mettre en scène
|
||
n'importe quel objet, pas seulement un personnage. -->
|
||
<details class="propGroup" open>
|
||
<summary>Informations</summary>
|
||
<div class="propGroupBody">
|
||
<div class="field">
|
||
<label class="label is-small">Nom de l'objet</label>
|
||
<div class="control"><input class="input is-small" type="text" id="scene-obj-name" value="{{ obj.name or '' }}" placeholder="ex. Le Sorcier" onchange="saveSceneObjectName({{ obj.id }}, this.value)"></div>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
{% endif %}
|
||
|
||
<details class="propGroup" open>
|
||
<summary>Position & 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 in ('dialogue_box', 'quiz_box', 'score_widget') %}
|
||
<!-- "🎨 Style" (voir screens/rendering/dialogue_box_style.py) —
|
||
PARTAGÉ par les 3 widgets d'interface : aucun n'a de collision
|
||
ni d'animation, juste une apparence — police, couleur du
|
||
texte, couleur de chaque zone (header/body/footer, "header_bg"
|
||
sert de fond unique pour "🏆 Score", qui n'a pas de body/footer). -->
|
||
<details class="propGroup" open>
|
||
<summary>Style</summary>
|
||
<div class="propGroupBody" data-element-id="{{ obj.id }}">
|
||
<div class="field">
|
||
<label class="label is-small">Police</label>
|
||
<div class="control">
|
||
<input class="input is-small" type="text" id="dbox-font-family" value="{{ dialogue_box_style.font_family }}" placeholder="ex. Georgia, serif" onchange="saveDialogueBoxStyle({{ obj.id }})">
|
||
</div>
|
||
</div>
|
||
<div class="columns is-multiline posGrid">
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Taille (px)</label>
|
||
<div class="control"><input class="input is-small" type="number" id="dbox-font-size" min="8" max="48" value="{{ dialogue_box_style.font_size }}" onchange="saveDialogueBoxStyle({{ obj.id }})"></div>
|
||
</div>
|
||
</div>
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Épaisseur</label>
|
||
<div class="control">
|
||
<select class="input is-small" id="dbox-font-weight" onchange="saveDialogueBoxStyle({{ obj.id }})">
|
||
{% for w in dialogue_box_font_weight_choices %}
|
||
<option value="{{ w }}" {{ 'selected' if dialogue_box_style.font_weight == w else '' }}>{{ 'Gras' if w == 'bold' else 'Normal' }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="columns is-multiline posGrid">
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Texte</label>
|
||
<div class="control"><input class="input is-small" type="color" id="dbox-text-color" value="{{ dialogue_box_style.text_color }}" onchange="saveDialogueBoxStyle({{ obj.id }})"></div>
|
||
</div>
|
||
</div>
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Header</label>
|
||
<div class="control"><input class="input is-small" type="color" id="dbox-header-bg" value="{{ dialogue_box_style.header_bg }}" onchange="saveDialogueBoxStyle({{ obj.id }})"></div>
|
||
</div>
|
||
</div>
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Corps</label>
|
||
<div class="control"><input class="input is-small" type="color" id="dbox-body-bg" value="{{ dialogue_box_style.body_bg }}" onchange="saveDialogueBoxStyle({{ obj.id }})"></div>
|
||
</div>
|
||
</div>
|
||
<div class="column is-half">
|
||
<div class="field"><label class="label is-small">Pied</label>
|
||
<div class="control"><input class="input is-small" type="color" id="dbox-footer-bg" value="{{ dialogue_box_style.footer_bg }}" onchange="saveDialogueBoxStyle({{ obj.id }})"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- RGAA/WCAG 1.4.3 : ces couleurs sont libres, sans aucune
|
||
garde-fou avant — un texte blanc sur fond blanc était
|
||
possible sans le moindre avertissement. Rempli par
|
||
checkDialogueBoxContrast() (scene-editor.js), appelée à
|
||
chaque changement de couleur ci-dessus. -->
|
||
<p class="help" id="dboxContrastWarning" style="color:#e0524d;"></p>
|
||
</div>
|
||
</details>
|
||
{% endif %}
|
||
|
||
{% if obj.kind not in ('fond', 'dialogue_box', 'quiz_box', 'score_widget') %}
|
||
<!-- 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 }}" data-commands-extra='{{ (personnage_commands.extra if personnage_commands else [])|tojson }}'>
|
||
<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, déjà connue de scene-editor.js, et
|
||
data-commands-extra ci-dessus, LU sur #personnageCommandsBody
|
||
plutôt que via une variable globale — voir
|
||
initPersonnageCommandsPanel(), rappelée après un changement
|
||
de sélection sans rechargement) — 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) }}";
|
||
// Borne le glisser-déposer/redimensionnement (static/js/scenes/
|
||
// scene-editor.js) au MONDE affiché (voir world_width/world_height,
|
||
// routes/scenes/scene_edit_view.py) — pas seulement à la scène
|
||
// nominale, sinon un "fond" plus grand qu'elle (voir "🎥 Champ de la
|
||
// caméra" ci-dessus) resterait injoignable par endroits une fois agrandi.
|
||
var SCENE_WIDTH = {{ world_width }};
|
||
var SCENE_HEIGHT = {{ world_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 }};
|
||
// "qui parle" dans l'éditeur de dialogue (voir "ℹ️ Informations" +
|
||
// static/js/triggers/trigger-editor.js::triggerSpeakerOptions) —
|
||
// n'importe quel objet de scène nommé, pas seulement un personnage.
|
||
var SCENE_OBJECT_NAMES_JSON = {{ scene_object_names_json|safe }};
|
||
// Assistant "condition" d'un déclencheur (voir static/js/scenes/
|
||
// collision-rules-editor.js::renderCollisionWizardConditionStep) — les
|
||
// noms des variables globales du jeu (voir l'onglet "Variables"
|
||
// ci-dessus), pour peupler son sélecteur de variable.
|
||
var GLOBAL_VARIABLE_NAMES = {{ global_variables | map(attribute='name') | list | tojson }};
|
||
// Nouvelles actions "surbrillance"/"vidéo" (voir static/js/scenes/
|
||
// collision-rules-editor.js) — même patron que les autres LABELS_MAP
|
||
// ci-dessus.
|
||
var SURBRILLANCE_LABELS_MAP = {{ surbrillance_labels_json|safe }};
|
||
var VIDEO_MODE_LABELS_MAP = {{ video_mode_labels_json|safe }};
|
||
// "Mes assets" (voir auth/user_assets_dir.py) — url déjà calculée côté
|
||
// serveur (url_for), pour peupler le sélecteur "asset_url" de "son"/
|
||
// "vidéo" sans redemander une URL au serveur à chaque ouverture de
|
||
// l'assistant.
|
||
var USER_ASSETS_OPTIONS = {{ user_assets_options_json|safe }};
|
||
// Section "Déclencheurs de l'écran" (voir static/js/triggers/
|
||
// trigger-editor.js::initScreenTriggersSection) — id de l'écran ouvert,
|
||
// nécessaire pour poster sur /game/<slug>/screens/<id>/triggers.
|
||
var CURRENT_SCREEN_ID = {{ screen.id }};
|
||
var SCREEN_TRIGGERS_INITIAL = {{ screen_triggers|tojson }};
|
||
</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/scenes/global-variables.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/triggers/trigger-editor.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/scenes/collision-rules-editor.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/ia/ia-chat.js') }}"></script>
|
||
{% endblock %}
|