Intègre un chat IA capable de manipuler la scène via les mêmes fonctions que l'éditeur manuel (objets, variables, déclencheurs, images générées), avec conversations multiples par écran façon Claude. Corrige au passage le rafraîchissement pjax hors-ordre, l'onglet IA/déclencheurs vide après sélection d'un objet, la comparaison de booléens dans les conditions, et le blocage du glisser-déposer hors du cadre caméra après un redimensionnement de fond par l'IA. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
662 lines
42 KiB
HTML
662 lines
42 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">
|
||
<div class="iconGallery personnageGallery">
|
||
{% for asset in user_assets %}
|
||
<button type="button" class="iconGalleryTile" title="{{ asset.original_name or 'Asset' }}"
|
||
onclick="addSceneObject('decor', null, null, {{ asset.id }})">
|
||
<img src="{{ url_for('serve_user_asset', user_id=asset.user_id, filename=asset.filename) }}"
|
||
alt="{{ asset.original_name or 'Asset' }}" class="personnageThumb">
|
||
</button>
|
||
{% endfor %}
|
||
</div>
|
||
<label class="button is-small is-fullwidth mt-2">
|
||
+ Importer une image
|
||
<input type="file" accept="image/*" style="display:none" onchange="uploadUserAsset(this)">
|
||
</label>
|
||
<p class="help">Réutilisable dans tous tes projets — jamais visible par les autres comptes.</p>
|
||
</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 }}" 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 %}
|
||
<!-- "🖥️ 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" onclick="addSceneObject('dialogue_box')">Boîte de dialogue</button>
|
||
<button type="button" class="button is-small is-fullwidth mb-2" onclick="addSceneObject('quiz_box')">Boîte à quiz</button>
|
||
<button type="button" class="button is-small is-fullwidth" 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;">
|
||
{% 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 }};
|
||
</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 %}
|