Enrichit les declencheurs/actions de scene (clic/survol/affichage, surbrillance/video/son/visibilite/indication/attendre) et fiabilise la pose d'un fond/decor importe
Build and deploy / test-python (push) Successful in 10m59s
Build and deploy / test-js (push) Successful in 1m27s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

- 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>
This commit is contained in:
william
2026-09-12 00:28:46 +02:00
co-authored by Claude Sonnet 5
parent b07b231a61
commit 559331f9cf
67 changed files with 4739 additions and 306 deletions
+2
View File
@@ -181,7 +181,9 @@
<script src="{{ asset_url('js/play/triggers.js') }}"></script>
<script src="{{ asset_url('js/play/personnage-controller.js') }}"></script>
<script src="{{ asset_url('js/play/dialogue-box-controller.js') }}"></script>
<script src="{{ asset_url('js/play/video-overlay.js') }}"></script>
<script src="{{ asset_url('js/play/collision-rules-controller.js') }}"></script>
<script src="{{ asset_url('js/play/screen-triggers-controller.js') }}"></script>
<script src="{{ asset_url('js/play/bindings.js') }}"></script>
<script src="{{ asset_url('js/play/flow-engine.js') }}"></script>
<script>
+143 -20
View File
@@ -75,20 +75,117 @@
<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>
<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
@@ -100,7 +197,9 @@
<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 }}')">
<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 %}
@@ -119,7 +218,9 @@
<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 }}')">
<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 %}
@@ -138,9 +239,15 @@
<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>
<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>
@@ -167,7 +274,8 @@
<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;">
<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/
@@ -647,6 +755,21 @@
// 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>