// ---------- Éditeur de scène 2D (fondations multi-éditeurs) ---------- // JS propre à templates/scene_edit.html : ajout/déplacement/redimension- // nement d'objets de scène EN PIXELS (pas de %, voir ensure_scene_schema.py), // mirror des fonctions équivalentes de static/js/screen_edit/tree-panels.js // pour l'éditeur document (onElementMouseDown/onResizeMouseDown/ // saveGeometry), mais sur une scène à taille FIXE plutôt qu'un canevas // fluide — la logique de flow elle-même (flow-editor.js, tabs-and-blocks.js, // animation-timeline.js, personnage-panel.js, personnage-preview.js) est // réutilisée TELLE QUELLE, chargée telle quelle par scene_edit.html. function addSceneObject(kind, forgeCharacter, backgroundSlug) { var form = document.createElement('form'); form.method = 'post'; form.action = "/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add"; var html = ''; if (forgeCharacter) html += ''; if (backgroundSlug) html += ''; form.innerHTML = html; document.body.appendChild(form); form.requestSubmit(); } 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 }), }); } function updateScenePosFields(x, y, w, h) { var xEl = document.getElementById('scene-pos-x'), yEl = document.getElementById('scene-pos-y'); var wEl = document.getElementById('scene-pos-w'), hEl = document.getElementById('scene-pos-h'); if (xEl) xEl.value = Math.round(x); if (yEl) yEl.value = Math.round(y); if (wEl) wEl.value = Math.round(w); if (hEl) hEl.value = Math.round(h); } // Glisser-déposer en pixels, clampé à la taille FIXE de la scène // (SCENE_WIDTH/SCENE_HEIGHT, voir templates/scene_edit.html) — mirror // d'onElementMouseDown() (tree-panels.js), mais sans conversion en % : // une scène a des coordonnées pixel absolues, pas fluides. function onSceneObjectMouseDown(e, id) { if (e.target.classList.contains('resizeHandle')) return; e.preventDefault(); var el = document.getElementById('el-' + id); var startX = e.clientX, startY = e.clientY; var startLeft = parseFloat(el.style.left) || 0, startTop = parseFloat(el.style.top) || 0; var width = parseFloat(el.style.width) || 0, height = parseFloat(el.style.height) || 0; var moved = false; function onMove(ev) { var dx = ev.clientX - startX, dy = ev.clientY - startY; if (Math.abs(dx) > 3 || Math.abs(dy) > 3) moved = true; var newLeft = Math.max(0, Math.min(SCENE_WIDTH - width, startLeft + dx)); var newTop = Math.max(0, Math.min(SCENE_HEIGHT - height, startTop + dy)); el.style.left = newLeft + 'px'; el.style.top = newTop + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); if (moved) { var newLeft = parseFloat(el.style.left), newTop = parseFloat(el.style.top); updateScenePosFields(newLeft, newTop, width, height); saveSceneObjectGeometry(id, newLeft, newTop, width, height); } else { window.location.href = SCREEN_EDIT_URL + '?selected=' + id; } } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } function onSceneObjectResizeMouseDown(e, id) { e.stopPropagation(); e.preventDefault(); var el = document.getElementById('el-' + id); var startX = e.clientX, startY = e.clientY; var startWidth = parseFloat(el.style.width) || 0, startHeight = parseFloat(el.style.height) || 0; var left = parseFloat(el.style.left) || 0, top = parseFloat(el.style.top) || 0; function onMove(ev) { var dx = ev.clientX - startX, dy = ev.clientY - startY; var newWidth = Math.max(8, Math.min(SCENE_WIDTH - left, startWidth + dx)); var newHeight = Math.max(8, Math.min(SCENE_HEIGHT - top, startHeight + dy)); el.style.width = newWidth + 'px'; el.style.height = newHeight + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); var newWidth = parseFloat(el.style.width), newHeight = parseFloat(el.style.height); updateScenePosFields(left, top, newWidth, newHeight); saveSceneObjectGeometry(id, left, top, newWidth, newHeight); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } // Champs numériques Position & taille du panneau de propriétés — mirror // du comportement équivalent pour l'éditeur document (voir l'IIFE dans // initBuilderPanel(), panel-init.js), mais ids/route distincts (pixels). (function () { var xEl = document.getElementById('scene-pos-x'), yEl = document.getElementById('scene-pos-y'); var wEl = document.getElementById('scene-pos-w'), hEl = document.getElementById('scene-pos-h'); if (!xEl || !wEl || !CURRENT_SELECTED_ID) return; function apply() { var canvasEl = document.getElementById('el-' + CURRENT_SELECTED_ID); var x = parseFloat(xEl.value) || 0, y = parseFloat(yEl.value) || 0; var w = parseFloat(wEl.value) || 1, h = parseFloat(hEl.value) || 1; if (canvasEl) { canvasEl.style.left = x + 'px'; canvasEl.style.top = y + 'px'; canvasEl.style.width = w + 'px'; canvasEl.style.height = h + 'px'; } saveSceneObjectGeometry(CURRENT_SELECTED_ID, x, y, w, h); } [xEl, yEl, wEl, hEl].forEach(function (el) { el.addEventListener('change', apply); }); })(); // Personnage d'un objet de scène : mirror de swapPersonnageCharacter() // (personnage-panel.js), mais POST vers /scene-objects/.../personnage // (voir routes/scenes/scene_object_personnage_data.py) plutôt que // /elements/.../personnage — même réponse (animations résolues), // relance l'aperçu animé via restartPersonnageIdlePreview() (personnage- // preview.js, déjà générique : ne dépend que de data-element-id/ // data-personnage, présents sur un objet de scène comme sur un élément). function swapSceneObjectCharacter(forgeCharacter) { var body = document.getElementById('personnagePanelBody'); if (!body) return; var objectId = parseInt(body.dataset.elementId, 10); document.querySelectorAll('#personnageGallerySwap .iconGalleryTile').forEach(function (tile) { tile.classList.toggle('selected', tile.getAttribute('onclick') === "swapSceneObjectCharacter('" + forgeCharacter + "')"); }); fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/personnage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source: 'forge', forge_character: forgeCharacter }), }) .then(function (r) { return r.json(); }) .then(function (json) { if (json.animations) { ELEMENT_ANIMATIONS_MAP[objectId] = json.animations; restartPersonnageIdlePreview(objectId, json.animations); } }); } // Rôle (voir screens/rendering/personnage_role.py) : seul un personnage // "Joueur" est déplacé/animé automatiquement au clavier — même patron AJAX // que swapSceneObjectCharacter() ci-dessus (POST JSON, aucun rechargement // de page), sur routes/scenes/scene_object_role.py. function saveSceneObjectRole(role) { var body = document.getElementById('personnagePanelBody'); if (!body) return; var objectId = parseInt(body.dataset.elementId, 10); fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/role', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ role: role }), }); } // Boîte de collision (voir screens/rendering/collision_settings.py, // static/js/play/conditions.js::elementsOverlap) : disponible pour TOUT // objet de scène (pas seulement "personnage") — même patron AJAX que les // fonctions ci-dessus, sur routes/scenes/scene_object_collision.py. // Recale aussi l'aperçu (#collisionBoxOverlay, seulement présent pour // l'objet sélectionné) pour rester en phase avec les champs, qu'ils // viennent d'être tapés à la main ou ajustés au glisser (voir // onCollisionBoxMouseDown/onCollisionBoxResizeMouseDown plus bas). function saveSceneObjectCollision() { var body = document.getElementById('collisionPanelBody'); if (!body) return; var objectId = parseInt(body.dataset.elementId, 10); var enabled = document.getElementById('collision-enabled').checked; var shape = document.getElementById('collision-shape').value; var width = parseFloat(document.getElementById('collision-width').value) || 0; var height = parseFloat(document.getElementById('collision-height').value) || 0; var offsetX = parseFloat(document.getElementById('collision-offset-x').value) || 0; var offsetY = parseFloat(document.getElementById('collision-offset-y').value) || 0; var overlay = document.getElementById('collisionBoxOverlay'); var objEl = document.getElementById('el-' + objectId); if (overlay && objEl) { var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0; overlay.style.width = width + 'px'; overlay.style.height = height + 'px'; overlay.style.left = ((objWidth - width) / 2 + offsetX) + 'px'; overlay.style.top = ((objHeight - height) / 2 + offsetY) + 'px'; overlay.classList.toggle('is-circle', shape === 'cercle'); overlay.classList.toggle('is-disabled', !enabled); } fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/collision', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: enabled, shape: shape, width: width, height: height, offset_x: offsetX, offset_y: offsetY }), }); } function updateCollisionFields(width, height, offsetX, offsetY) { var wEl = document.getElementById('collision-width'), hEl = document.getElementById('collision-height'); var oxEl = document.getElementById('collision-offset-x'), oyEl = document.getElementById('collision-offset-y'); if (wEl) wEl.value = Math.round(width); if (hEl) hEl.value = Math.round(height); if (oxEl) oxEl.value = Math.round(offsetX); if (oyEl) oyEl.value = Math.round(offsetY); } // Glisser le CORPS de l'aperçu ajuste son décalage (offset_x/offset_y) — // mirror d'onSceneObjectMouseDown ci-dessus, mais sur la boîte de // collision plutôt que l'objet lui-même (jamais le même geste : cliquer // l'objet EN DEHORS de la boîte de collision continue de le déplacer // normalement, la boîte est un enfant plus petit posé par-dessus). function onCollisionBoxMouseDown(e, id) { e.stopPropagation(); if (e.target.classList.contains('collisionResizeHandle')) return; e.preventDefault(); var overlay = document.getElementById('collisionBoxOverlay'); var objEl = document.getElementById('el-' + id); var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0; var startX = e.clientX, startY = e.clientY; var startLeft = parseFloat(overlay.style.left) || 0, startTop = parseFloat(overlay.style.top) || 0; var boxWidth = parseFloat(overlay.style.width) || 0, boxHeight = parseFloat(overlay.style.height) || 0; function onMove(ev) { overlay.style.left = (startLeft + (ev.clientX - startX)) + 'px'; overlay.style.top = (startTop + (ev.clientY - startY)) + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); var offsetX = (parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2; var offsetY = (parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2; updateCollisionFields(boxWidth, boxHeight, offsetX, offsetY); saveSceneObjectCollision(); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } // Glisser la poignée (coin bas-droit) redimensionne la boîte de collision // — mirror d'onSceneObjectResizeMouseDown, ancré sur son coin haut-gauche // (le centre se déplace donc légèrement à chaque redimensionnement, sauf // à recalculer l'offset pour le garder fixe : on choisit de garder le // COIN fixe, plus prévisible au glisser qu'un centre qui "fuit"). function onCollisionBoxResizeMouseDown(e, id) { e.stopPropagation(); e.preventDefault(); var overlay = document.getElementById('collisionBoxOverlay'); var objEl = document.getElementById('el-' + id); var objWidth = parseFloat(objEl.style.width) || 0, objHeight = parseFloat(objEl.style.height) || 0; var startX = e.clientX, startY = e.clientY; var startWidth = parseFloat(overlay.style.width) || 0, startHeight = parseFloat(overlay.style.height) || 0; var left = parseFloat(overlay.style.left) || 0, top = parseFloat(overlay.style.top) || 0; function onMove(ev) { overlay.style.width = Math.max(4, startWidth + (ev.clientX - startX)) + 'px'; overlay.style.height = Math.max(4, startHeight + (ev.clientY - startY)) + 'px'; } function onUp() { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); var newWidth = parseFloat(overlay.style.width), newHeight = parseFloat(overlay.style.height); var offsetX = left - (objWidth - newWidth) / 2; var offsetY = top - (objHeight - newHeight) / 2; updateCollisionFields(newWidth, newHeight, offsetX, offsetY); saveSceneObjectCollision(); } document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); } // "Commandes" (voir static/js/play/personnage-controller.js) : touches de // déplacement/interaction + blocage d'axe + touches supplémentaires d'un // personnage — un seul formulaire, enregistré en AJAX à chaque // changement de champ, même patron que swapSceneObjectCharacter() // ci-dessus (POST JSON vers routes/scenes/scene_object_commands.py, // aucun rechargement de page). function saveSceneObjectCommands() { var body = document.getElementById('personnageCommandsBody'); if (!body) return; var objectId = parseInt(body.dataset.elementId, 10); var extra = []; document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach(function (row) { var toucheEl = row.querySelector('.personnageKeyCapture'); var animEl = row.querySelector('.cmdExtraAnim'); if (toucheEl && toucheEl.value && animEl && animEl.value) { extra.push({ touche: toucheEl.value, animation: animEl.value }); } }); fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/commandes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ haut: document.getElementById('cmd-haut').value, bas: document.getElementById('cmd-bas').value, gauche: document.getElementById('cmd-gauche').value, droite: document.getElementById('cmd-droite').value, interagir: document.getElementById('cmd-interagir').value, axe: document.getElementById('cmd-axe').value, vitesse: document.getElementById('cmd-vitesse').value, extra: extra, }), }); } // Détection de touche (bug remonté par l'utilisateur : taper le nom d'une // touche à la main est source d'erreurs — "Espace" vs " ", "flèche haut" // vs "ArrowUp"...) : au focus, le champ (readonly, jamais tapé) affiche un // indice puis se remplit avec la PROCHAINE touche pressée (event.key, // même valeur que heldKeys/bindKeyboardTriggers, triggers.js — cohérent // avec ce que le moteur compare réellement au jeu). capture:true sur // window pour intercepter la touche avant tout autre raccourci de // l'éditeur (ex. Suppr qui efface l'objet sélectionné). function bindPersonnageKeyCapture(el) { if (!el || el.dataset.keyCaptureBound) return; el.dataset.keyCaptureBound = '1'; el.addEventListener('focus', function () { var previous = el.value; el.value = 'Appuie sur une touche...'; el.classList.add('is-warning'); function onKey(ev) { ev.preventDefault(); ev.stopPropagation(); window.removeEventListener('keydown', onKey, true); el.classList.remove('is-warning'); if (ev.key === 'Escape') { el.value = previous; el.blur(); return; } el.value = ev.key; el.blur(); el.dispatchEvent(new Event('change')); } window.addEventListener('keydown', onKey, true); }); } // Une ligne = une touche supplémentaire liée à UNE pose au choix parmi // celles RÉELLEMENT disponibles pour CE personnage (ELEMENT_ANIMATIONS_MAP, // déjà connu de personnage-panel.js/personnage-preview.js) — jamais un nom // tapé à la main, qui pourrait ne correspondre à aucune pose existante. function personnageCommandExtraRowEl(objectId, touche, animation) { var row = document.createElement('div'); row.className = 'field is-grouped personnageCommandExtraRow'; var animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {}); var options = animNames.map(function (name) { return ''; }).join(''); row.innerHTML = '
' + '