Remplace le bouton conditionnel "Agrandir la caméra à la zone visible" (n'apparaissait que si un fond dépassait déjà la scène) par deux champs "Caméra (px)" TOUJOURS visibles, pré-remplis avec scene_width/ scene_height actuels — demande explicite : "l'écran de l'éditeur a une taille fixe, la caméra a la même taille, c'est tout", un réglage direct plutôt qu'une suggestion automatique liée à la taille d'un fond. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
528 lines
26 KiB
JavaScript
528 lines
26 KiB
JavaScript
// ---------- É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 = '<input type="hidden" name="kind" value="' + kind + '">';
|
||
if (forgeCharacter) html += '<input type="hidden" name="forge_character" value="' + forgeCharacter + '">';
|
||
if (backgroundSlug) html += '<input type="hidden" name="background_slug" value="' + backgroundSlug + '">';
|
||
form.innerHTML = html;
|
||
document.body.appendChild(form);
|
||
form.requestSubmit();
|
||
}
|
||
|
||
// Taille de la caméra (voir "🎮 Jeu 2D" en haut de templates/
|
||
// scene_edit.html) — TOUJOURS modifiable ici, directement en pixels
|
||
// (demande explicite : "l'écran de l'éditeur a une taille fixe, la
|
||
// caméra a la même taille, c'est tout") : recharge la page une fois
|
||
// appliqué, SCENE_WIDTH/SCENE_HEIGHT et le cadre "🎥 Champ de la caméra"
|
||
// éventuel doivent tous deux refléter la nouvelle taille, plus simple
|
||
// qu'une mise à jour partielle en JS.
|
||
function applyCameraSize() {
|
||
var wEl = document.getElementById('scene-camera-w'), hEl = document.getElementById('scene-camera-h');
|
||
var width = parseInt(wEl.value, 10) || 1, height = parseInt(hEl.value, 10) || 1;
|
||
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-size", {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ width: width, height: height }),
|
||
}).then(function () { window.location.reload(); });
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
// Défilement automatique de .canvasFrame (overflow:auto, voir static/
|
||
// style.css) pendant un glisser/redimensionnement — bug corrigé : une
|
||
// scène à taille FIXE (960×540 par ex.) peut être plus LARGE que la zone
|
||
// visible de .canvasFrame (fenêtre pas assez large, panneaux ouverts...),
|
||
// auquel cas sa partie droite/basse défile HORS de vue. Sans défilement
|
||
// automatique, le curseur atteignait le bord de la zone visible (donc de
|
||
// .canvasFrame) bien avant celui de la scène elle-même — "impossible
|
||
// d'aller plus loin vers la droite" alors qu'il restait pourtant de la
|
||
// place dans la scène. `onNudge(dx, dy)` est rappelé à chaque tick tant
|
||
// que le curseur reste dans la marge d'un bord, MÊME SI la souris ne
|
||
// bouge plus (immobile près du bord) — mirror du comportement standard
|
||
// d'un glisser-déposer proche d'un bord de zone scrollable.
|
||
function forgeStartCanvasAutoScroll(getMouse, onNudge) {
|
||
var MARGIN = 40, SPEED = 14;
|
||
var timer = setInterval(function () {
|
||
var frame = document.querySelector('.canvasFrame');
|
||
if (!frame) return;
|
||
var rect = frame.getBoundingClientRect();
|
||
var mouse = getMouse();
|
||
var dx = 0, dy = 0;
|
||
if (mouse.x > rect.right - MARGIN) dx = SPEED;
|
||
else if (mouse.x < rect.left + MARGIN) dx = -SPEED;
|
||
if (mouse.y > rect.bottom - MARGIN) dy = SPEED;
|
||
else if (mouse.y < rect.top + MARGIN) dy = -SPEED;
|
||
if (!dx && !dy) return;
|
||
frame.scrollLeft = Math.max(0, frame.scrollLeft + dx);
|
||
frame.scrollTop = Math.max(0, frame.scrollTop + dy);
|
||
onNudge(dx, dy);
|
||
}, 30);
|
||
return function stop() { clearInterval(timer); };
|
||
}
|
||
|
||
// 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. Position
|
||
// ACCUMULÉE (curLeft/curTop, jamais recalculée depuis le point de départ
|
||
// fixe) : c'est ce qui permet à forgeStartCanvasAutoScroll ci-dessus de
|
||
// continuer à la déplacer même quand la souris ne bouge plus (immobile
|
||
// près d'un bord).
|
||
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 lastClientX = e.clientX, lastClientY = e.clientY;
|
||
var curLeft = parseFloat(el.style.left) || 0, curTop = parseFloat(el.style.top) || 0;
|
||
var width = parseFloat(el.style.width) || 0, height = parseFloat(el.style.height) || 0;
|
||
var moved = false;
|
||
// Les panneaux flottants "🧩 Objets"/"⚙️ Propriétés" (voir static/style.css
|
||
// .floatPanel, z-index:60) restent DÉLIBÉRÉMENT en position:fixed
|
||
// PAR-DESSUS le canevas, pour maximiser l'espace utile — mais un objet
|
||
// glissé sous l'un d'eux, à son z-index habituel (bien plus bas), y
|
||
// disparaissait littéralement à l'écran PENDANT le glisser (toujours
|
||
// déplacé/enregistré correctement en dessous, juste invisible pendant
|
||
// le geste) : impossible de voir où on le dépose. Passé au-dessus des
|
||
// panneaux (z-index très élevé) le temps du glisser SEULEMENT, restauré
|
||
// à sa valeur d'origine à la fin.
|
||
var previousZIndex = el.style.zIndex;
|
||
|
||
// Bug corrigé ("toujours bloqué", confirmé lié à l'introduction du fond/
|
||
// caméra, voir add_scene_object.py) : un "fond" est posé à sa taille
|
||
// RÉELLE (souvent bien plus grande que la scène, ex. 1920px pour une
|
||
// scène de 960px — exprès, pour que la caméra puisse le suivre en
|
||
// défilant) — Math.max(0, Math.min(SCENE_WIDTH - width, ...)) suppose
|
||
// SCENE_WIDTH - width POSITIF (objet plus petit que la scène) : pour un
|
||
// objet plus GRAND, cette différence est négative, et Math.max(0, ...)
|
||
// ramenait alors TOUJOURS la position à 0 quel que soit le glisser — un
|
||
// fond (ou tout autre objet plus grand que la scène) restait donc figé
|
||
// à (0,0), impossible à repositionner. min/max sont maintenant calculés
|
||
// dans le bon ordre quel que soit lequel des deux est le plus grand.
|
||
function applyDelta(dx, dy) {
|
||
var minLeft = Math.min(0, SCENE_WIDTH - width), maxLeft = Math.max(0, SCENE_WIDTH - width);
|
||
var minTop = Math.min(0, SCENE_HEIGHT - height), maxTop = Math.max(0, SCENE_HEIGHT - height);
|
||
curLeft = Math.max(minLeft, Math.min(maxLeft, curLeft + dx));
|
||
curTop = Math.max(minTop, Math.min(maxTop, curTop + dy));
|
||
el.style.left = curLeft + 'px';
|
||
el.style.top = curTop + 'px';
|
||
}
|
||
function onMove(ev) {
|
||
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
|
||
lastClientX = ev.clientX; lastClientY = ev.clientY;
|
||
if (Math.abs(ev.clientX - startX) > 3 || Math.abs(ev.clientY - startY) > 3) {
|
||
if (!moved) el.style.zIndex = 9999;
|
||
moved = true;
|
||
}
|
||
applyDelta(dx, dy);
|
||
}
|
||
var stopAutoScroll = forgeStartCanvasAutoScroll(
|
||
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
|
||
);
|
||
function onUp() {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
stopAutoScroll();
|
||
if (moved) {
|
||
el.style.zIndex = previousZIndex;
|
||
updateScenePosFields(curLeft, curTop, width, height);
|
||
saveSceneObjectGeometry(id, curLeft, curTop, 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 lastClientX = e.clientX, lastClientY = e.clientY;
|
||
var curWidth = parseFloat(el.style.width) || 0, curHeight = parseFloat(el.style.height) || 0;
|
||
var left = parseFloat(el.style.left) || 0, top = parseFloat(el.style.top) || 0;
|
||
// Voir la même note dans onSceneObjectMouseDown ci-dessus : au-dessus
|
||
// des panneaux flottants le temps du geste, pour rester visible même
|
||
// en redimensionnant sous l'un d'eux.
|
||
var previousZIndex = el.style.zIndex;
|
||
el.style.zIndex = 9999;
|
||
|
||
// Pas de plafond à "SCENE_WIDTH - left" ici (bug corrigé) : un objet
|
||
// (typiquement un "fond", voir add_scene_object.py) doit pouvoir être
|
||
// redimensionné plus GRAND que la scène elle-même — c'est précisément
|
||
// le principe de la caméra qui suit le personnage sur un monde plus
|
||
// grand que le viewport. Seul un minimum (8px, jamais un objet
|
||
// invisible) reste imposé.
|
||
function applyDelta(dx, dy) {
|
||
curWidth = Math.max(8, curWidth + dx);
|
||
curHeight = Math.max(8, curHeight + dy);
|
||
el.style.width = curWidth + 'px';
|
||
el.style.height = curHeight + 'px';
|
||
}
|
||
function onMove(ev) {
|
||
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
|
||
lastClientX = ev.clientX; lastClientY = ev.clientY;
|
||
applyDelta(dx, dy);
|
||
}
|
||
var stopAutoScroll = forgeStartCanvasAutoScroll(
|
||
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
|
||
);
|
||
function onUp() {
|
||
document.removeEventListener('mousemove', onMove);
|
||
document.removeEventListener('mouseup', onUp);
|
||
stopAutoScroll();
|
||
el.style.zIndex = previousZIndex;
|
||
var newWidth = curWidth, newHeight = curHeight;
|
||
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 '<option value="' + name + '"' + (name === animation ? ' selected' : '') + '>' + name + '</option>';
|
||
}).join('');
|
||
row.innerHTML =
|
||
'<div class="control"><input class="input is-small personnageKeyCapture" readonly placeholder="Touche..." value="' + (touche || '').replace(/"/g, '"') + '"></div>' +
|
||
'<div class="control"><div class="select is-small"><select class="cmdExtraAnim">' + options + '</select></div></div>' +
|
||
'<div class="control"><button type="button" class="button is-small" onclick="this.closest(\'.personnageCommandExtraRow\').remove(); saveSceneObjectCommands();">×</button></div>';
|
||
var keyInput = row.querySelector('.personnageKeyCapture');
|
||
bindPersonnageKeyCapture(keyInput);
|
||
keyInput.addEventListener('change', saveSceneObjectCommands);
|
||
row.querySelector('.cmdExtraAnim').addEventListener('change', saveSceneObjectCommands);
|
||
return row;
|
||
}
|
||
|
||
function addPersonnageCommandExtraRow() {
|
||
var body = document.getElementById('personnageCommandsBody');
|
||
var container = document.getElementById('personnageCommandsExtra');
|
||
if (!body || !container) return;
|
||
var objectId = parseInt(body.dataset.elementId, 10);
|
||
container.appendChild(personnageCommandExtraRowEl(objectId, '', ''));
|
||
}
|
||
|
||
// "ℹ️ Informations" (voir templates/scene_edit.html) : le nom d'un
|
||
// personnage, réutilisé comme "qui parle" dans l'éditeur de dialogue de
|
||
// quête (voir static/js/quests/quest-editor.js).
|
||
function saveSceneObjectName(objectId, name) {
|
||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/name', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: name }),
|
||
});
|
||
}
|
||
|
||
// "🎨 Style" du widget "💬 Boîte de dialogue" (voir screens/rendering/
|
||
// dialogue_box_style.py) — envoie TOUJOURS les 7 champs ensemble (même
|
||
// convention "replace-all" que saveSceneObjectCollision), jamais un seul
|
||
// champ isolé.
|
||
function saveDialogueBoxStyle(objectId) {
|
||
var style = {
|
||
font_family: document.getElementById('dbox-font-family').value,
|
||
font_size: Number(document.getElementById('dbox-font-size').value),
|
||
font_weight: document.getElementById('dbox-font-weight').value,
|
||
text_color: document.getElementById('dbox-text-color').value,
|
||
header_bg: document.getElementById('dbox-header-bg').value,
|
||
body_bg: document.getElementById('dbox-body-bg').value,
|
||
footer_bg: document.getElementById('dbox-footer-bg').value,
|
||
};
|
||
var el = document.getElementById('el-' + objectId);
|
||
var widget = el && el.querySelector('.dialogueBoxWidget');
|
||
if (widget) {
|
||
widget.style.fontFamily = style.font_family;
|
||
widget.style.fontSize = style.font_size + 'px';
|
||
widget.style.fontWeight = style.font_weight;
|
||
widget.style.color = style.text_color;
|
||
var header = widget.querySelector('.dialogueBoxHeader'); if (header) header.style.background = style.header_bg;
|
||
var b = widget.querySelector('.dialogueBoxBody'); if (b) b.style.background = style.body_bg;
|
||
var footer = widget.querySelector('.dialogueBoxFooter'); if (footer) footer.style.background = style.footer_bg;
|
||
}
|
||
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/dialogue-box-style', {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ style: style }),
|
||
});
|
||
}
|
||
|
||
(function () {
|
||
var body = document.getElementById('personnageCommandsBody');
|
||
if (!body) return;
|
||
var objectId = parseInt(body.dataset.elementId, 10);
|
||
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach(function (id) {
|
||
bindPersonnageKeyCapture(document.getElementById(id));
|
||
});
|
||
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach(function (id) {
|
||
var el = document.getElementById(id);
|
||
if (el) el.addEventListener('change', saveSceneObjectCommands);
|
||
});
|
||
var container = document.getElementById('personnageCommandsExtra');
|
||
(window.PERSONNAGE_COMMANDS_EXTRA || []).forEach(function (e) {
|
||
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
|
||
});
|
||
})();
|