Supprime le type d'écran "document" et recentre le produit sur le jeu 2D
Build and deploy / test-python (push) Successful in 9m41s
Build and deploy / test-js (push) Successful in 1m0s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped

Le document de cadrage produit cible des formateurs non techniques créant
des serious games/quiz gamifiés — l'éditeur générique "document" (blocs de
logique en nœuds, timeline d'animation, définitions d'objets/relations,
templates réutilisables) est une complexité hors cible que l'effort
d'ingénierie récent avait déjà abandonnée au profit du jeu_2d.

- Onboarding : ne garde que le parcours "RPG" (jeu_2d), retire
  Quiz/Embranchement/Créer mon jeu de A à Z (tous document-only)
- Suppression en bloc des modules exclusifs au document : routes/elements,
  routes/element_types, routes/objects, routes/legacy_actions,
  screens/elements, screens/element_types, screens/widgets,
  screens/legacy_actions, le rendu render_element_html.py et son cluster,
  templates/screen_edit.html, templates/game_dashboard.html,
  flow-editor.js/tabs-and-blocks.js/animation-timeline.js
- Dashboard toujours simplifié (un seul mode possible désormais)
- Tests document-only supprimés, tests de logique partagée (flow,
  événements personnalisés, animations) retargetés sur des écrans jeu_2d
- Aucune régression jeu_2d : 299 tests passent

Carte d'onboarding retravaillée : argumentaire RH non technique (liste à
coche, badge "Compatible LMS"), taille et interaction de retournement
ajustées.
This commit is contained in:
william
2026-09-04 20:56:32 +02:00
parent 00f7191a8b
commit 50835a18e2
221 changed files with 1239 additions and 12245 deletions
+184 -16
View File
@@ -8,16 +8,97 @@
// animation-timeline.js, personnage-panel.js, personnage-preview.js) est
// réutilisée TELLE QUELLE, chargée telle quelle par scene_edit.html.
// AJAX (voir routes/scenes/scene_object_add.py — répond en JSON dès que
// "X-Requested-With: fetch" est posé) : pose l'objet SANS recharger la
// page, pour ne plus faire "disparaître" un instant l'éditeur de collision/
// quêtes (état JS entier réinitialisé par un rechargement complet) ni
// perdre l'onglet/le scroll en cours. Le nouvel objet n'est PAS
// 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) {
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();
var body = new FormData();
body.append('kind', kind);
if (forgeCharacter) body.append('forge_character', forgeCharacter);
if (backgroundSlug) body.append('background_slug', backgroundSlug);
fetch("/game/" + GAME_SLUG + "/screens/" + SCREEN_ID + "/scene-objects/add", {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body: body,
})
.then(function (r) {
// Secours si le serveur répond encore par une redirection HTML
// classique (ex. déploiement pas encore à jour côté serveur alors
// que ce script, lui, l'est déjà) : l'objet est de toute façon déjà
// posé en base à ce stade (voir screens/scenes/add_scene_object.py),
// seul un rechargement peut alors l'afficher.
var contentType = r.headers.get('content-type') || '';
if (!r.ok || contentType.indexOf('application/json') === -1) {
window.location.reload();
return null;
}
return r.json();
})
.then(function (json) {
if (!json) return;
var canvas = document.getElementById('canvas');
var el = document.createElement('div');
el.className = 'canvasElement';
el.id = 'el-' + json.id;
el.style.position = 'absolute';
el.style.left = json.x + 'px'; el.style.top = json.y + 'px';
el.style.width = json.width + 'px'; el.style.height = json.height + 'px';
el.setAttribute('onmousedown', 'onSceneObjectMouseDown(event, ' + json.id + ')');
el.innerHTML =
'<div class="canvasElementInner">' + json.rendered_html + '</div>' +
'<div class="resizeHandle" onmousedown="onSceneObjectResizeMouseDown(event, ' + json.id + ')"></div>';
canvas.appendChild(el);
refreshCollisionRuleObjects();
if (json.animations) {
ELEMENT_ANIMATIONS_MAP[json.id] = json.animations;
restartPersonnageIdlePreview(json.id, json.animations);
}
})
// Filet de sécurité : mieux vaut un rechargement (l'objet est déjà en
// base à ce stade) qu'un bouton "+ Ajouter" qui semble ne plus rien
// faire du tout si jamais une erreur imprévue survient ici.
.catch(function () { window.location.reload(); });
}
// AJAX (voir routes/scenes/scene_object_delete.py) : bouton 🗑️ du panneau
// de propriétés (toujours l'objet SÉLECTIONNÉ, seul cas où ce bouton est
// affiché) — supprime l'objet SANS recharger la page ni même redemander
// #builder3 au serveur (déjà retiré du DOM ici) : ne plus faire "vider"
// l'éditeur de collision/quêtes le temps d'un remplacement de <main>,
// comme le faisait la soumission de formulaire classique interceptée par
// pjax.js (voir son commentaire d'en-tête).
function deleteSceneObject(id) {
fetch('/game/' + GAME_SLUG + '/scene-objects/' + id + '/delete', {
method: 'POST', headers: { 'X-Requested-With': 'fetch' },
})
.then(function (r) {
var contentType = r.headers.get('content-type') || '';
if (!r.ok || contentType.indexOf('application/json') === -1) {
window.location.reload();
return null;
}
return r.json();
})
.then(function (json) {
if (!json || !json.ok) return;
var el = document.getElementById('el-' + id);
if (el) el.remove();
if (typeof _stopPersonnageIdlePreview === 'function') _stopPersonnageIdlePreview(id);
refreshCollisionRuleObjects();
// L'objet supprimé était forcément le sélectionné (seul cas où 🗑️
// est affiché) — referme le panneau de propriétés et l'URL, sans
// requête supplémentaire au serveur.
var propsBody = document.querySelector('#propsPanelFloat .floatPanelBody');
if (propsBody) {
propsBody.innerHTML = '<div class="hint" style="padding:20px 0;">Sélectionne un objet (sur la scène ou dans la liste) pour voir et modifier ses propriétés ici.</div>';
}
CURRENT_SELECTED_ID = null;
history.replaceState(null, '', SCREEN_EDIT_URL);
})
.catch(function () { window.location.reload(); });
}
// Taille de la caméra (voir "🎮 Jeu 2D" en haut de templates/
@@ -152,7 +233,17 @@ function onSceneObjectMouseDown(e, id) {
updateScenePosFields(curLeft, curTop, width, height);
saveSceneObjectGeometry(id, curLeft, curTop, width, height);
} else {
window.location.href = SCREEN_EDIT_URL + '?selected=' + id;
// goToSelected() (static/js/screen_edit/tree-panels.js, déjà utilisée
// par l'éditeur document) : va chercher #builder3 en arrière-plan et
// le remplace sur place plutôt que de recharger toute la page —
// demande explicite ("à chaque fois que je clique sur un élément la
// page se recharge"). openPropsPanel() : si le panneau était fermé
// (état mémorisé dans localStorage, voir restoreFloatPanelState()),
// cliquer un objet doit le rouvrir pour montrer ses propriétés —
// sinon initBuilderPanel() (rappelé par goToSelected) le laissait
// fermé, panneau à jour mais invisible.
openPropsPanel();
goToSelected(id);
}
}
document.addEventListener('mousemove', onMove);
@@ -208,7 +299,7 @@ function onSceneObjectResizeMouseDown(e, id) {
// 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 () {
function initScenePosFields() {
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;
@@ -223,7 +314,8 @@ function onSceneObjectResizeMouseDown(e, id) {
saveSceneObjectGeometry(CURRENT_SELECTED_ID, x, y, w, h);
}
[xEl, yEl, wEl, hEl].forEach(function (el) { el.addEventListener('change', apply); });
})();
}
initScenePosFields();
// Personnage d'un objet de scène : mirror de swapPersonnageCharacter()
// (personnage-panel.js), mais POST vers /scene-objects/.../personnage
@@ -263,7 +355,25 @@ function saveSceneObjectRole(role) {
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/role', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: role }),
});
}).then(refreshCollisionRuleObjects);
}
// Réutilisé après ajout/suppression/changement de rôle ou de nom (voir
// routes/scenes/scene_collision_rule_objects.py) : un changement de rôle
// peut faire apparaître/disparaître une carte (passage à/depuis "joueur",
// jamais dans "🧩 Collision") et un changement de nom change son libellé
// affiché — recalculée entièrement côté serveur plutôt que rejouée en JS
// (même logique déjà nécessaire pour le rôle implicite d'une scène posée
// avant ce réglage, voir build_collision_rule_objects) SANS recharger la
// page.
function refreshCollisionRuleObjects() {
if (typeof COLLISION_RULE_OBJECTS === 'undefined') return;
fetch('/game/' + GAME_SLUG + '/screens/' + SCREEN_ID + '/collision-rule-objects')
.then(function (r) { return r.json(); })
.then(function (json) {
COLLISION_RULE_OBJECTS = json;
renderCollisionRuleCards();
});
}
// Boîte de collision (voir screens/rendering/collision_settings.py,
@@ -475,9 +585,27 @@ function saveSceneObjectName(objectId, name) {
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/name', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name }),
}).then(function () {
refreshCollisionRuleObjects();
refreshSceneObjectNames();
});
}
// SCENE_OBJECT_NAMES (voir routes/scenes/scene_object_names_list.py) :
// posée UNE SEULE FOIS au chargement complet de la page (<script> inline
// en bas, HORS de #builder3, jamais rafraîchie par une simple sélection
// sans rechargement, voir goToSelected()) — sans ce rafraîchissement,
// renommer un personnage puis ouvrir la modale de dialogue dans la MÊME
// session ne faisait jamais apparaître son nouveau nom dans "qui parle"
// (voir questSpeakerOptions(), static/js/quests/quest-editor.js) tant
// qu'aucun vrai rechargement de page n'avait eu lieu entre temps.
function refreshSceneObjectNames() {
if (typeof SCENE_OBJECT_NAMES === 'undefined') return;
fetch('/game/' + GAME_SLUG + '/scene-object-names')
.then(function (r) { return r.json(); })
.then(function (json) { SCENE_OBJECT_NAMES = json; });
}
// "🎨 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
@@ -509,7 +637,7 @@ function saveDialogueBoxStyle(objectId) {
});
}
(function () {
function initPersonnageCommandsPanel() {
var body = document.getElementById('personnageCommandsBody');
if (!body) return;
var objectId = parseInt(body.dataset.elementId, 10);
@@ -521,7 +649,47 @@ function saveDialogueBoxStyle(objectId) {
if (el) el.addEventListener('change', saveSceneObjectCommands);
});
var container = document.getElementById('personnageCommandsExtra');
(window.PERSONNAGE_COMMANDS_EXTRA || []).forEach(function (e) {
// Lu depuis data-commands-extra (posé sur CET élément par le serveur,
// voir templates/scene_edit.html) plutôt que la variable globale
// PERSONNAGE_COMMANDS_EXTRA (déclarée dans le <script> inline en bas de
// page, HORS de #builder3) : cette fonction est aussi rappelée après un
// changement de sélection SANS rechargement (voir initSceneBuilderPanel
// ci-dessous), qui ne remplace que #builder3 — cette variable globale
// resterait alors bloquée sur les commandes du PRÉCÉDENT objet
// sélectionné.
var extra = [];
try { extra = JSON.parse(body.dataset.commandsExtra || '[]'); } catch (e) { extra = []; }
extra.forEach(function (e) {
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
});
})();
}
initPersonnageCommandsPanel();
// Rappelée après chaque changement de sélection d'objet de scène SANS
// rechargement de page (voir onSceneObjectMouseDown -> goToSelected(),
// static/js/screen_edit/tree-panels.js::refreshBuilderPanel, qui l'appelle
// juste après avoir remplacé #builder3 et initBuilderPanel()) — reproduit
// ici ce que ce script fait déjà tout seul au chargement initial de la
// page (voir initScenePosFields()/initPersonnageCommandsPanel() ci-dessus)
// + relance l'aperçu animé de tous les personnages du canevas (leurs <img>
// viennent d'être recréés par le même remplacement, voir
// startAllPersonnagePreviews()/personnage-preview.js).
//
// .builderCanvasArea (les onglets "🖥️ Scène"/"🧩 Collision"/"🗺️ Quêtes"/
// "📣 Événements") est ENTIÈREMENT à l'intérieur de #builder3 (voir
// templates/scene_edit.html) — le remplacer réinjecte donc aussi
// #collisionRulesCards/le conteneur des quêtes tels que le SERVEUR les
// rend, c'est-à-dire VIDES (ces listes sont rendues entièrement en JS,
// jamais en Jinja, voir collision-rules-editor.js/quest-editor.js) : sans
// les redessiner ici, les onglets "🧩 Collision"/"🗺️ Quêtes" semblaient se
// "vider" à chaque sélection d'un objet de scène. COLLISION_RULE_OBJECTS/
// QUESTS_JSON eux-mêmes ne sont PAS affectés par ce remplacement (déclarés
// dans le <script> inline en bas de page, HORS de #builder3) — il suffit
// donc de rejouer leur rendu, pas de les recharger.
function initSceneBuilderPanel() {
initScenePosFields();
initPersonnageCommandsPanel();
if (typeof startAllPersonnagePreviews === 'function') startAllPersonnagePreviews();
if (typeof renderCollisionRuleCards === 'function') renderCollisionRuleCards();
if (typeof renderQuestListPage === 'function') renderQuestListPage();
}