Fait de tout le "monde" affiché par l'éditeur (la taille réelle d'un "fond" plus grand que la scène nominale, voir routes/scenes/ scene_edit_view.py::world_width/world_height) la scène/caméra ELLE-MÊME — jusqu'ici scene_width/scene_height n'étaient fixées qu'à la création de l'écran, jamais modifiables ensuite. Le bouton n'apparaît que quand un fond dépasse encore la scène nominale (même condition que le repère "🎥 Champ de la caméra"), affiche la taille cible, recharge la page une fois appliqué. La caméra ne recadre alors plus rien en jeu (monde == scène == viewport, voir personnage-controller.js::forgeUpdateSceneCamera). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
608 lines
38 KiB
HTML
608 lines
38 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') }}">
|
||
<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; }
|
||
/* Même correctif que ci-dessus, pour le widget "💬 Boîte de dialogue"
|
||
(screens/scenes/render_scene_object.py, un <div>, pas un <img>). */
|
||
.canvasElementInner > .dialogueBoxWidget, .canvasElementInner > .quizBoxWidget, .canvasElementInner > .scoreWidget{
|
||
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; display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap;">
|
||
<span>🎮 Jeu 2D — scène à taille fixe ({{ screen.scene_width }}×{{ screen.scene_height }} px).</span>
|
||
{% if world_width > screen.scene_width or world_height > screen.scene_height %}
|
||
<!-- "📐 Agrandir la caméra à la zone visible" : fait de tout le
|
||
"monde" affiché (voir world_width/world_height, routes/scenes/
|
||
scene_edit_view.py) la scène/caméra elle-même — n'apparaît que si
|
||
un fond dépasse encore la scène nominale (voir "🎥 Champ de la
|
||
caméra" plus bas), inutile sinon. -->
|
||
<button type="button" class="button is-small" onclick="expandCameraToWorld({{ world_width }}, {{ world_height }})">📐 Agrandir la caméra à la zone visible ({{ world_width }}×{{ world_height }} px)</button>
|
||
{% endif %}
|
||
</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 %}
|
||
<!-- "🖥️ 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 "🗺️ Quêtes") — 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="collision-rules" onclick="switchBuilderTab('collision-rules')">🧩 Collision</button>
|
||
<button type="button" class="builderTabBtn" data-tab="quests" onclick="switchBuilderTab('quests')">🗺️ Quêtes</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:{{ 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>
|
||
|
||
<!-- É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>
|
||
|
||
<!-- Éditeur de quêtes (voir db/quests/) : une LISTE (comme l'onglet
|
||
Événements juste après), pas une grille de cartes — chaque ligne
|
||
résume titre/objectif/récompense/statut/résultat, cliquer dessus
|
||
ouvre la modale d'arbre de dialogue (static/js/quests/
|
||
quest-editor.js, partagée avec l'assistant "+ Action" de l'onglet
|
||
Collision). Rendu ENTIÈREMENT en JS depuis QUESTS_JSON, même
|
||
convention que #collisionRulesCards ci-dessus. -->
|
||
<div class="builderTabPanel" id="questsTabPanel" data-tab-panel="quests">
|
||
<div class="dashPanelHeader">
|
||
<h2>🗺️ Quêtes</h2>
|
||
<button type="button" class="button is-small primary" onclick="createQuestFromListPage()">+ Créer une quête</button>
|
||
</div>
|
||
<p class="hint">Titre, objectif, récompense, statut, résultat — clique sur une ligne pour écrire son enchaînement de dialogue.</p>
|
||
<div id="questsListWrap"></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 %}
|
||
{% 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;">{{ '🏃' if obj.kind == 'personnage' else _WIDGET_ICONS.get(obj.kind, '🖼️') }} {{ obj.name or ('Personnage' if obj.kind == 'personnage' else _WIDGET_LABELS.get(obj.kind, '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>
|
||
|
||
{% 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 de quête (voir "🗺️ Quêtes", static/js/quests/
|
||
quest-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>
|
||
</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 }}">
|
||
<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) }}";
|
||
// 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 }};
|
||
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 }};
|
||
// "qui parle" dans l'éditeur de dialogue (voir "ℹ️ Informations" +
|
||
// static/js/quests/quest-editor.js::questSpeakerOptions) — n'importe
|
||
// quel objet de scène nommé, pas seulement un personnage.
|
||
var SCENE_OBJECT_NAMES_JSON = {{ scene_object_names_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 %}
|