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
+121 -4
View File
@@ -16,7 +16,7 @@
// automatiquement sélectionné/ouvert dans le panneau de propriétés (voir
// onSceneObjectMouseDown, qui recharge encore la page pour ça) — juste
// posé sur le canevas, cliquable ensuite comme n'importe quel autre.
function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId) {
function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
var body = new FormData();
body.append('kind', kind);
if (forgeCharacter) body.append('forge_character', forgeCharacter);
@@ -24,6 +24,10 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId) {
// "Mes assets" (voir auth/user_assets_dir.py) : une image importée par
// le créateur ou générée par l'IA, distincte de BACKGROUND_LIBRARY.
if (assetId) body.append('asset_id', assetId);
// Vrai glisser-déposer (voir onCanvasDrop ci-dessous) : position EXACTE
// du dépôt — absent pour un simple clic sur la galerie, qui garde le
// défaut habituel (100,100, voir screens/scenes/add_scene_object.py).
if (x != null && y != null) { body.append('x', x); body.append('y', y); }
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add", {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
})
@@ -65,18 +69,113 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId) {
.catch(function () { window.location.reload(); });
}
// ---------- Vrai glisser-déposer depuis la galerie (demande explicite :
// "n'importe où sur la scène", pas seulement un clic qui pose à une
// position par défaut) ----------
// dataTransfer ne porte que des CHAÎNES : les 4 arguments d'addSceneObject
// sont sérialisés en JSON plutôt qu'éclatés en plusieurs champs (certains
// sont `null` — JSON les distingue proprement d'une chaîne vide, un
// simple attribut HTML ne le pourrait pas). `draggable="true"` sur un
// <button> coexiste nativement avec son `onclick` (un clic sans
// mouvement déclenche toujours "click", un glisser réel "dragstart" —
// jamais les deux à la fois) : le clic simple (pose à la position par
// défaut) continue donc de fonctionner exactement comme avant.
function onGalleryTileDragStart(ev, kind, forgeCharacter, backgroundSlug, assetId) {
ev.dataTransfer.effectAllowed = 'copy';
ev.dataTransfer.setData('application/json', JSON.stringify({
kind: kind, forgeCharacter: forgeCharacter || null,
backgroundSlug: backgroundSlug || null, assetId: assetId || null,
}));
}
// Un navigateur refuse "drop" par défaut sur TOUT élément — preventDefault()
// sur "dragover" est ce qui l'autorise explicitement ici (voir MDN, aucun
// contournement plus simple).
function onCanvasDragOver(ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
}
function onCanvasDrop(ev) {
ev.preventDefault();
var raw = ev.dataTransfer.getData('application/json');
if (!raw) return;
var data;
try { data = JSON.parse(raw); } catch (e) { return; }
var canvas = document.getElementById('canvas');
var rect = canvas.getBoundingClientRect();
// Pas de zoom/échelle CSS sur #canvas (voir templates/scene_edit.html,
// juste width/height en px fixes) : une simple soustraction suffit,
// aucune division par un facteur d'échelle à appliquer. getBoundingClientRect()
// intègre déjà nativement le défilement de .canvasFrame.
var x = ev.clientX - rect.left;
var y = ev.clientY - rect.top;
addSceneObject(data.kind, data.forgeCharacter, data.backgroundSlug, data.assetId, x, y);
}
// "Mes assets" (voir routes/assets/upload_user_asset.py, panneau "Mes
// assets" de scene_edit.html) : un simple rechargement après l'upload —
// la galerie est rendue côté serveur (user_assets), pas la peine de la
// reconstruire en JS pour un flux qui n'arrive pas souvent.
function uploadUserAsset(inputEl) {
// `sceneKind` ("fond"/"decor", images uniquement — demande explicite :
// "un bouton d'import séparé pour les images de fond et les objets") :
// décide comment un CLIC sur la tuile résultante pose l'objet (voir
// screens/scenes/add_scene_object.py) — absent pour un son/une vidéo,
// qui n'ont rien à "poser".
function uploadUserAsset(inputEl, sceneKind) {
var file = inputEl.files && inputEl.files[0];
if (!file) return;
var body = new FormData();
body.append('file', file);
if (sceneKind) body.append('scene_kind', sceneKind);
fetch('/assets/upload', { method: 'POST', body: body })
.then(function (r) { return r.json(); })
.then(function () { window.location.reload(); })
.then(function (r) {
// Bug corrigé : un rechargement inconditionnel masquait toute
// erreur serveur (ex. son de plus de 3 minutes, voir
// auth/validate_audio_duration.py) — le fichier semblait juste
// "disparaître" sans explication, jamais ajouté à la galerie.
return r.json().then(function (json) { return { ok: r.ok, json: json }; });
})
.then(function (result) {
if (!result.ok) { alert(result.json.error || "Impossible d'importer ce fichier."); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
}
// Suppression d'un fichier de "Mes assets" (demande explicite : "voir,
// utiliser ou supprimer des son, image et vidéo") — voir
// routes/assets/delete_user_asset.py. Un simple rechargement après coup,
// comme uploadUserAsset() juste au-dessus : ce panneau ne se recompose
// pas assez souvent pour justifier une suppression ciblée du DOM.
function deleteUserAsset(assetId, name) {
if (!confirm('Supprimer "' + name + '" ? Cette action est définitive.')) return;
fetch('/assets/' + assetId, { method: 'DELETE' })
.then(function (r) { return r.json().then(function (json) { return { ok: r.ok, json: json }; }); })
.then(function (result) {
if (!result.ok) { alert(result.json.error || "Impossible de supprimer ce fichier."); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
}
// "→ Fond"/"→ Décor" (demande diagnostiquée depuis un "décalage" : une
// image importée par le mauvais bouton, ou avant la séparation fond/décor,
// restait bloquée dans la mauvaise section, impossible à poser comme fond
// sans la ré-importer en double) — voir auth/update_user_asset_scene_kind.py.
// Ne touche jamais un objet DÉJÀ posé sur une scène : juste la classification
// de l'ASSET, pour que la prochaine pose utilise la bonne section/le bon
// rendu (object-fit:fill pour un fond, :contain pour un décor).
function reclassifyUserAsset(assetId, sceneKind) {
fetch('/assets/' + assetId + '/scene-kind', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scene_kind: sceneKind }),
})
.then(function (r) { return r.json().then(function (json) { return { ok: r.ok, json: json }; }); })
.then(function (result) {
if (!result.ok) { alert(result.json.error || "Reclassement impossible."); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
}
@@ -137,6 +236,24 @@ function saveSceneObjectGeometry(id, x, y, width, height) {
return fetch("/game/" + GAME_SLUG + "/scene-objects/" + id + "/geometry", {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ x: x, y: y, width: width, height: height }),
}).then(function (r) { return r.json(); }).then(function (data) {
// Bug corrigé (même famille que iaChatRefreshAfterTurn(), voir
// static/js/ia/ia-chat.js) : un "fond" redimensionné/déplacé à la
// souris au-delà du champ de la caméra doit élargir le "monde" tout
// de suite — sans ça, window.SCENE_WIDTH/SCENE_HEIGHT (posées une
// seule fois à l'ouverture de la page) restaient à leur ancienne
// valeur et le glisser-déposer continuait de tout borner à l'ancien
// cadre, rendant injoignable à la souris tout ce qui dépassait
// désormais la scène nominale.
if (data && typeof data.world_width === 'number' && typeof data.world_height === 'number') {
window.SCENE_WIDTH = data.world_width;
window.SCENE_HEIGHT = data.world_height;
var canvas = document.getElementById('canvas');
if (canvas) {
canvas.style.width = data.world_width + 'px';
canvas.style.height = data.world_height + 'px';
}
}
});
}