Simplification demandée pour l'éditeur de jeu RPG : un personnage posé sur une scène 2D se déplace et s'anime TOUT SEUL avec ZQSD (+ E pour interagir) dès qu'il est posé — plus besoin de poser le moindre nœud de flow pour un mouvement de base. Le panneau "🕹️ Commandes" du personnage permet de remapper les 4 touches de déplacement et la touche d'interaction, et de bloquer un axe (horizontal/vertical seulement). Entièrement client (static/js/play/personnage-controller.js), réutilise heldKeys (triggers.js) et applyObjectProperty/clampSceneObjectPosition/ runSpriteAnimation (actions.js) — aucune logique dupliquée, et ça fonctionne aussi bien en ligne que dans l'export Web/SCORM (aucune requête serveur). "walk"/"idle"/"interact" (poses déjà présentes pour tout personnage Forge/CraftPix) sont utilisées telles quelles ; une pose absente dégrade silencieusement (déplacement sans animation) plutôt que de planter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
178 lines
8.3 KiB
JavaScript
178 lines
8.3 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) {
|
|
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 + '">';
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
// "Commandes" (voir static/js/play/personnage-controller.js) : touches de
|
|
// déplacement/interaction + blocage d'axe 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);
|
|
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,
|
|
}),
|
|
});
|
|
}
|
|
|
|
(function () {
|
|
var body = document.getElementById('personnageCommandsBody');
|
|
if (!body) return;
|
|
['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);
|
|
});
|
|
})();
|