Phase 3 : hardening qualite de code - typage strict, securite, dead code, a11y

Config strictement stricte partout (ruff, mypy --strict, bandit, vulture,
import-linter, eslint, stylelint), aucune regle desactivee "pour ne pas
casser le build" - l'existant a ete corrige pour la satisfaire plutot que
l'inverse. Hooks pre-commit locaux (language: system) bloquants.

- Typage mypy --strict propage a tout le moteur (db, screens, auth, core,
  ai, routes, puis publish/scripts/tests/app.py/build_css.py).
- Securite : fuite de handle fichier Windows corrigee dans l'export SCORM
  (routes/publish/export_scorm.py), CSRF/RNG non-crypto/xAPI documentes
  (# nosec, # NOSONAR justifies), nouveau db.json_for_script() (echappe
  "</script>" dans le JSON embarque en <script>, 25 sites).
- Architecture : imports circulaires/F811 nettoyes, contrats
  import-linter respectes, code mort retire (vulture).
- Accessibilite : 69 champs de formulaire sans label correctement
  associe corriges (for/id ou aria-label) sur 11 templates.
- ESLint/Stylelint : lot mecanique JS/CSS, regles ajustees puis
  appliquees (aucune desactivee sans verification individuelle).
- Tests : isolation du compte admin partage (nettoyage ponctuel +
  fixture de teardown automatique en filet de securite), suite complete
  verte (591 tests Python, 241 tests JS).
- SonarQube Community Build self-heberge (Docker + PostgreSQL) : rapport
  complet analyse point par point, faux positifs documentes.
- .gitattributes ajoute (LF force) : core.autocrlf=true sur cette machine
  faisait echouer ESLint (linebreak-style) via un bug connu de git
  (checkout "en place" qui ignore l'eol force sur un fichier deja
  present sur disque - contourne en supprimant puis recreant chaque
  fichier suivi).

djLint (H021, styles inline) volontairement saute pour ce commit -
backlog assume, deja documente, traite dans un lot separe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
william
2026-09-15 16:06:15 +02:00
co-authored by Claude Sonnet 5
parent 7db4803b93
commit c57420c8c9
454 changed files with 16448 additions and 6967 deletions
+281 -239
View File
@@ -17,7 +17,7 @@
// 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, x, y) {
var body = new FormData();
const body = new FormData();
body.append('kind', kind);
if (forgeCharacter) body.append('forge_character', forgeCharacter);
if (backgroundSlug) body.append('background_slug', backgroundSlug);
@@ -28,35 +28,34 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
// 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,
fetch(`/game/${GAME_SLUG}/screens/${SCREEN_ID}/scene-objects/add`, {
method: 'POST', headers: { 'X-Requested-With': 'fetch' }, body,
})
.then(function (r) {
.then((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') || '';
const 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) {
.then((json) => {
if (!json) return;
var canvas = document.getElementById('canvas');
var el = document.createElement('div');
const canvas = document.getElementById('canvas');
const el = document.createElement('div');
el.className = 'canvasElement';
el.id = 'el-' + json.id;
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>';
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);
if (json.animations) {
ELEMENT_ANIMATIONS_MAP[json.id] = json.animations;
@@ -66,7 +65,7 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
// 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(); });
.catch(() => { window.location.reload(); });
}
// ---------- Vrai glisser-déposer depuis la galerie (demande explicite :
@@ -83,8 +82,10 @@ function addSceneObject(kind, forgeCharacter, backgroundSlug, assetId, x, y) {
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,
kind,
forgeCharacter: forgeCharacter || null,
backgroundSlug: backgroundSlug || null,
assetId: assetId || null,
}));
}
@@ -98,18 +99,18 @@ function onCanvasDragOver(ev) {
function onCanvasDrop(ev) {
ev.preventDefault();
var raw = ev.dataTransfer.getData('application/json');
const raw = ev.dataTransfer.getData('application/json');
if (!raw) return;
var data;
let data;
try { data = JSON.parse(raw); } catch (e) { return; }
var canvas = document.getElementById('canvas');
var rect = canvas.getBoundingClientRect();
const canvas = document.getElementById('canvas');
const 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;
const x = ev.clientX - rect.left;
const y = ev.clientY - rect.top;
addSceneObject(data.kind, data.forgeCharacter, data.backgroundSlug, data.assetId, x, y);
}
@@ -123,24 +124,23 @@ function onCanvasDrop(ev) {
// 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];
const file = inputEl.files && inputEl.files[0];
if (!file) return;
var body = new FormData();
const body = new FormData();
body.append('file', file);
if (sceneKind) body.append('scene_kind', sceneKind);
fetch('/assets/upload', { method: 'POST', body: body })
.then(function (r) {
fetch('/assets/upload', { method: 'POST', body })
.then((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) {
r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || "Impossible d'importer ce fichier."); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// Suppression d'un fichier de "Mes assets" (demande explicite : "voir,
@@ -149,14 +149,14 @@ function uploadUserAsset(inputEl, sceneKind) {
// 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; }
if (!window.confirm(`Supprimer "${name}" ? Cette action est définitive.`)) return;
fetch(`/assets/${assetId}`, { method: 'DELETE' })
.then((r) => r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || 'Impossible de supprimer ce fichier.'); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// "→ Fond"/"→ Décor" (demande diagnostiquée depuis un "décalage" : une
@@ -167,16 +167,17 @@ function deleteUserAsset(assetId, name) {
// 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' },
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; }
.then((r) => r.json().then((json) => ({ ok: r.ok, json })))
.then((result) => {
if (!result.ok) { alert(result.json.error || 'Reclassement impossible.'); return; }
window.location.reload();
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// AJAX (voir routes/scenes/scene_object_delete.py) : bouton 🗑️ du panneau
@@ -187,33 +188,33 @@ function reclassifyUserAsset(assetId, sceneKind) {
// 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', {
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') || '';
.then((r) => {
const 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) {
.then((json) => {
if (!json || !json.ok) return;
var el = document.getElementById('el-' + id);
const el = document.getElementById(`el-${id}`);
if (el) el.remove();
if (typeof _stopPersonnageIdlePreview === 'function') _stopPersonnageIdlePreview(id);
// 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');
const 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);
window.history.replaceState(null, '', SCREEN_EDIT_URL);
})
.catch(function () { window.location.reload(); });
.catch(() => { window.location.reload(); });
}
// Taille de la caméra (voir "🎮 Jeu 2D" en haut de templates/
@@ -224,19 +225,25 @@ function deleteSceneObject(id) {
// é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(); });
const wEl = document.getElementById('scene-camera-w'); const
hEl = document.getElementById('scene-camera-h');
const width = parseInt(wEl.value, 10) || 1; const
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, height }),
}).then(() => { 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 }),
}).then(function (r) { return r.json(); }).then(function (data) {
return fetch(`/game/${GAME_SLUG}/scene-objects/${id}/geometry`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
x, y, width, height,
}),
}).then((r) => r.json()).then((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
@@ -248,18 +255,20 @@ function saveSceneObjectGeometry(id, x, y, width, height) {
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');
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.style.width = data.world_width + 'px';
canvas.style.height = data.world_height + 'px';
canvas.style.width = `${data.world_width}px`;
canvas.style.height = `${data.world_height}px`;
}
}
});
}
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');
const xEl = document.getElementById('scene-pos-x'); const
yEl = document.getElementById('scene-pos-y');
const wEl = document.getElementById('scene-pos-w'); const
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);
@@ -279,13 +288,15 @@ function updateScenePosFields(x, y, w, h) {
// 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');
const MARGIN = 40; const
SPEED = 14;
const timer = setInterval(() => {
const frame = document.querySelector('.canvasFrame');
if (!frame) return;
var rect = frame.getBoundingClientRect();
var mouse = getMouse();
var dx = 0, dy = 0;
const rect = frame.getBoundingClientRect();
const mouse = getMouse();
let dx = 0; let
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;
@@ -309,12 +320,16 @@ function forgeStartCanvasAutoScroll(getMouse, onNudge) {
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;
const el = document.getElementById(`el-${id}`);
const startX = e.clientX; const
startY = e.clientY;
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curLeft = parseFloat(el.style.left) || 0; let
curTop = parseFloat(el.style.top) || 0;
const width = parseFloat(el.style.width) || 0; const
height = parseFloat(el.style.height) || 0;
let 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
@@ -324,7 +339,7 @@ function onSceneObjectMouseDown(e, id) {
// 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;
const 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
@@ -338,15 +353,18 @@ function onSceneObjectMouseDown(e, id) {
// à (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);
const minLeft = Math.min(0, SCENE_WIDTH - width); const
maxLeft = Math.max(0, SCENE_WIDTH - width);
const minTop = Math.min(0, SCENE_HEIGHT - height); const
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';
el.style.left = `${curLeft}px`;
el.style.top = `${curTop}px`;
}
function onMove(ev) {
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
const dx = ev.clientX - lastClientX; const
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;
@@ -354,9 +372,7 @@ function onSceneObjectMouseDown(e, id) {
}
applyDelta(dx, dy);
}
var stopAutoScroll = forgeStartCanvasAutoScroll(
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
);
const stopAutoScroll = forgeStartCanvasAutoScroll(() => ({ x: lastClientX, y: lastClientY }), applyDelta);
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
@@ -386,14 +402,17 @@ function onSceneObjectMouseDown(e, id) {
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;
const el = document.getElementById(`el-${id}`);
let lastClientX = e.clientX; let
lastClientY = e.clientY;
let curWidth = parseFloat(el.style.width) || 0; let
curHeight = parseFloat(el.style.height) || 0;
const left = parseFloat(el.style.left) || 0; const
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;
const previousZIndex = el.style.zIndex;
el.style.zIndex = 9999;
// Pas de plafond à "SCENE_WIDTH - left" ici (bug corrigé) : un objet
@@ -405,23 +424,23 @@ function onSceneObjectResizeMouseDown(e, id) {
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';
el.style.width = `${curWidth}px`;
el.style.height = `${curHeight}px`;
}
function onMove(ev) {
var dx = ev.clientX - lastClientX, dy = ev.clientY - lastClientY;
const dx = ev.clientX - lastClientX; const
dy = ev.clientY - lastClientY;
lastClientX = ev.clientX; lastClientY = ev.clientY;
applyDelta(dx, dy);
}
var stopAutoScroll = forgeStartCanvasAutoScroll(
function () { return { x: lastClientX, y: lastClientY }; }, applyDelta
);
const stopAutoScroll = forgeStartCanvasAutoScroll(() => ({ 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;
const newWidth = curWidth; const
newHeight = curHeight;
updateScenePosFields(left, top, newWidth, newHeight);
saveSceneObjectGeometry(id, left, top, newWidth, newHeight);
}
@@ -433,20 +452,24 @@ function onSceneObjectResizeMouseDown(e, id) {
// du comportement équivalent pour l'éditeur document (voir l'IIFE dans
// initBuilderPanel(), panel-init.js), mais ids/route distincts (pixels).
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');
const xEl = document.getElementById('scene-pos-x'); const
yEl = document.getElementById('scene-pos-y');
const wEl = document.getElementById('scene-pos-w'); const
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;
const canvasEl = document.getElementById(`el-${CURRENT_SELECTED_ID}`);
const x = parseFloat(xEl.value) || 0; const
y = parseFloat(yEl.value) || 0;
const w = parseFloat(wEl.value) || 1; const
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';
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); });
[xEl, yEl, wEl, hEl].forEach((el) => { el.addEventListener('change', apply); });
}
initScenePosFields();
@@ -458,18 +481,19 @@ initScenePosFields();
// 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');
const 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 + "')");
const objectId = parseInt(body.dataset.elementId, 10);
document.querySelectorAll('#personnageGallerySwap .iconGalleryTile').forEach((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' },
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) {
.then((r) => r.json())
.then((json) => {
if (json.animations) {
ELEMENT_ANIMATIONS_MAP[objectId] = json.animations;
restartPersonnageIdlePreview(objectId, json.animations);
@@ -482,12 +506,13 @@ function swapSceneObjectCharacter(forgeCharacter) {
// 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');
const 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 }),
const 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 }),
});
}
@@ -500,37 +525,43 @@ function saveSceneObjectRole(role) {
// viennent d'être tapés à la main ou ajustés au glisser (voir
// onCollisionBoxMouseDown/onCollisionBoxResizeMouseDown plus bas).
function saveSceneObjectCollision() {
var body = document.getElementById('collisionPanelBody');
const 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;
const objectId = parseInt(body.dataset.elementId, 10);
const enabled = document.getElementById('collision-enabled').checked;
const shape = document.getElementById('collision-shape').value;
const width = parseFloat(document.getElementById('collision-width').value) || 0;
const height = parseFloat(document.getElementById('collision-height').value) || 0;
const offsetX = parseFloat(document.getElementById('collision-offset-x').value) || 0;
const offsetY = parseFloat(document.getElementById('collision-offset-y').value) || 0;
var overlay = document.getElementById('collisionBoxOverlay');
var objEl = document.getElementById('el-' + objectId);
const overlay = document.getElementById('collisionBoxOverlay');
const 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';
const objWidth = parseFloat(objEl.style.width) || 0; const
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 }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/collision`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
enabled, shape, width, 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');
const wEl = document.getElementById('collision-width'); const
hEl = document.getElementById('collision-height');
const oxEl = document.getElementById('collision-offset-x'); const
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);
@@ -546,22 +577,26 @@ 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;
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startLeft = parseFloat(overlay.style.left) || 0; const
startTop = parseFloat(overlay.style.top) || 0;
const boxWidth = parseFloat(overlay.style.width) || 0; const
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';
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;
const offsetX = (parseFloat(overlay.style.left) || 0) - (objWidth - boxWidth) / 2;
const offsetY = (parseFloat(overlay.style.top) || 0) - (objHeight - boxHeight) / 2;
updateCollisionFields(boxWidth, boxHeight, offsetX, offsetY);
saveSceneObjectCollision();
}
@@ -577,23 +612,28 @@ function onCollisionBoxMouseDown(e, id) {
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;
const overlay = document.getElementById('collisionBoxOverlay');
const objEl = document.getElementById(`el-${id}`);
const objWidth = parseFloat(objEl.style.width) || 0; const
objHeight = parseFloat(objEl.style.height) || 0;
const startX = e.clientX; const
startY = e.clientY;
const startWidth = parseFloat(overlay.style.width) || 0; const
startHeight = parseFloat(overlay.style.height) || 0;
const left = parseFloat(overlay.style.left) || 0; const
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';
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;
const newWidth = parseFloat(overlay.style.width); const
newHeight = parseFloat(overlay.style.height);
const offsetX = left - (objWidth - newWidth) / 2;
const offsetY = top - (objHeight - newHeight) / 2;
updateCollisionFields(newWidth, newHeight, offsetX, offsetY);
saveSceneObjectCollision();
}
@@ -608,19 +648,20 @@ function onCollisionBoxResizeMouseDown(e, id) {
// ci-dessus (POST JSON vers routes/scenes/scene_object_commands.py,
// aucun rechargement de page).
function saveSceneObjectCommands() {
var body = document.getElementById('personnageCommandsBody');
const 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');
const objectId = parseInt(body.dataset.elementId, 10);
const extra = [];
document.querySelectorAll('#personnageCommandsExtra .personnageCommandExtraRow').forEach((row) => {
const toucheEl = row.querySelector('.personnageKeyCapture');
const 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' },
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,
@@ -629,7 +670,7 @@ function saveSceneObjectCommands() {
interagir: document.getElementById('cmd-interagir').value,
axe: document.getElementById('cmd-axe').value,
vitesse: document.getElementById('cmd-vitesse').value,
extra: extra,
extra,
}),
});
}
@@ -645,8 +686,8 @@ function saveSceneObjectCommands() {
function bindPersonnageKeyCapture(el) {
if (!el || el.dataset.keyCaptureBound) return;
el.dataset.keyCaptureBound = '1';
el.addEventListener('focus', function () {
var previous = el.value;
el.addEventListener('focus', () => {
const previous = el.value;
el.value = 'Appuie sur une touche...';
el.classList.add('is-warning');
function onKey(ev) {
@@ -668,17 +709,14 @@ function bindPersonnageKeyCapture(el) {
// 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');
const 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, '&quot;') + '"></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');
const animNames = Object.keys(ELEMENT_ANIMATIONS_MAP[objectId] || {});
const options = animNames.map((name) => `<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, '&quot;')}"></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>';
const keyInput = row.querySelector('.personnageKeyCapture');
bindPersonnageKeyCapture(keyInput);
keyInput.addEventListener('change', saveSceneObjectCommands);
row.querySelector('.cmdExtraAnim').addEventListener('change', saveSceneObjectCommands);
@@ -686,10 +724,10 @@ function personnageCommandExtraRowEl(objectId, touche, animation) {
}
function addPersonnageCommandExtraRow() {
var body = document.getElementById('personnageCommandsBody');
var container = document.getElementById('personnageCommandsExtra');
const body = document.getElementById('personnageCommandsBody');
const container = document.getElementById('personnageCommandsExtra');
if (!body || !container) return;
var objectId = parseInt(body.dataset.elementId, 10);
const objectId = parseInt(body.dataset.elementId, 10);
container.appendChild(personnageCommandExtraRowEl(objectId, '', ''));
}
@@ -697,9 +735,10 @@ function addPersonnageCommandExtraRow() {
// personnage, réutilisé comme "qui parle" dans l'éditeur de dialogue d'un
// déclencheur (voir static/js/triggers/trigger-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 }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/name`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
}).then(refreshSceneObjectNames);
}
@@ -714,9 +753,9 @@ function saveSceneObjectName(objectId, name) {
// 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; });
fetch(`/game/${GAME_SLUG}/scene-object-names`)
.then((r) => r.json())
.then((json) => { SCENE_OBJECT_NAMES = json; });
}
// RGAA/WCAG 1.4.3 (contraste minimum) : les couleurs du "🎨 Style"
@@ -727,16 +766,16 @@ function refreshSceneObjectNames() {
// son choix esthétique, ex. un jeu volontairement sombre/stylisé).
function forgeHexToRgb(hex) {
hex = String(hex || '').replace('#', '');
if (hex.length === 3) hex = hex.split('').map(function (c) { return c + c; }).join('');
var num = parseInt(hex, 16);
if (isNaN(num) || hex.length !== 6) return [0, 0, 0];
if (hex.length === 3) hex = hex.split('').map((c) => c + c).join('');
const num = parseInt(hex, 16);
if (Number.isNaN(num) || hex.length !== 6) return [0, 0, 0];
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
}
function forgeRelativeLuminance(rgb) {
var srgb = rgb.map(function (c) {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const srgb = rgb.map((c) => {
c /= 255;
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
}
@@ -744,23 +783,24 @@ function forgeRelativeLuminance(rgb) {
// Formule de contraste WCAG standard (relative luminance) — 4.5:1 est le
// seuil "AA" pour du texte normal (RGAA 3.2/critère 1.4.3).
function forgeContrastRatio(hex1, hex2) {
var l1 = forgeRelativeLuminance(forgeHexToRgb(hex1));
var l2 = forgeRelativeLuminance(forgeHexToRgb(hex2));
var lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
const l1 = forgeRelativeLuminance(forgeHexToRgb(hex1));
const l2 = forgeRelativeLuminance(forgeHexToRgb(hex2));
const lighter = Math.max(l1, l2); const
darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
function checkDialogueBoxContrast(style) {
var warningEl = document.getElementById('dboxContrastWarning');
const warningEl = document.getElementById('dboxContrastWarning');
if (!warningEl) return;
var ratios = [
const ratios = [
forgeContrastRatio(style.text_color, style.header_bg),
forgeContrastRatio(style.text_color, style.body_bg),
forgeContrastRatio(style.text_color, style.footer_bg),
];
var minRatio = Math.min.apply(null, ratios);
const minRatio = Math.min.apply(null, ratios);
warningEl.textContent = minRatio < 4.5
? '⚠️ Contraste insuffisant (' + minRatio.toFixed(1) + ':1, minimum recommandé 4.5:1) — le texte risque d\'être difficile à lire.'
? `⚠️ Contraste insuffisant (${minRatio.toFixed(1)}:1, minimum recommandé 4.5:1) — le texte risque d'être difficile à lire.`
: '';
}
@@ -769,7 +809,7 @@ function checkDialogueBoxContrast(style) {
// convention "replace-all" que saveSceneObjectCollision), jamais un seul
// champ isolé.
function saveDialogueBoxStyle(objectId) {
var style = {
const 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,
@@ -779,20 +819,21 @@ function saveDialogueBoxStyle(objectId) {
footer_bg: document.getElementById('dbox-footer-bg').value,
};
checkDialogueBoxContrast(style);
var el = document.getElementById('el-' + objectId);
var widget = el && el.querySelector('.dialogueBoxWidget');
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.dialogueBoxWidget');
if (widget) {
widget.style.fontFamily = style.font_family;
widget.style.fontSize = style.font_size + 'px';
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;
const header = widget.querySelector('.dialogueBoxHeader'); if (header) header.style.background = style.header_bg;
const b = widget.querySelector('.dialogueBoxBody'); if (b) b.style.background = style.body_bg;
const 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 }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/dialogue-box-style`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ style }),
});
}
@@ -804,11 +845,11 @@ function saveDialogueBoxStyle(objectId) {
// grille (data-selected-template) puis sauvegarde tout de suite (même
// geste qu'un clic sur une tuile de personnage/décor).
function _selectQuizBoxTemplate(objectId, key, gridId) {
var grid = document.getElementById(gridId);
const grid = document.getElementById(gridId);
if (grid) {
grid.setAttribute('data-selected-template', key);
var tiles = grid.querySelectorAll('.quizTemplateTile');
for (var i = 0; i < tiles.length; i++) {
const tiles = grid.querySelectorAll('.quizTemplateTile');
for (let i = 0; i < tiles.length; i++) {
tiles[i].classList.toggle('is-selected', tiles[i].getAttribute('data-template') === key);
}
}
@@ -841,47 +882,48 @@ function selectQuizBoxPageTemplate(objectId, key) { _selectQuizBoxTemplate(objec
// galerie de modèles à afficher change entièrement (boîte <-> page) :
// un simple patch DOM ne pourrait recalculer ni l'un ni l'autre ici.
function saveQuizBoxConfig(objectId) {
var meta = document.getElementById('quizBoxTemplateMeta');
const meta = document.getElementById('quizBoxTemplateMeta');
function currentTemplate(gridId, metaAttr) {
var grid = document.getElementById(gridId);
const grid = document.getElementById(gridId);
if (!grid) return meta ? meta.getAttribute(metaAttr) : null;
var selectedTile = grid.querySelector('.quizTemplateTile.is-selected');
const selectedTile = grid.querySelector('.quizTemplateTile.is-selected');
return grid.getAttribute('data-selected-template') || (selectedTile && selectedTile.getAttribute('data-template'));
}
var fullscreenEl = document.getElementById('quiz-fullscreen');
var fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.getAttribute('data-was-checked') === '1');
var config = {
const fullscreenEl = document.getElementById('quiz-fullscreen');
const fullscreenChanged = fullscreenEl.checked !== (fullscreenEl.getAttribute('data-was-checked') === '1');
const config = {
fullscreen: fullscreenEl.checked,
timer_mode: document.getElementById('quiz-timer-mode').value,
timer_seconds: Number(document.getElementById('quiz-timer-seconds').value),
dialog_template: currentTemplate('quizDialogTemplateGrid', 'data-dialog-template') || 'defaut',
page_template: currentTemplate('quizPageTemplateGrid', 'data-page-template') || 'manga',
};
fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/quiz-config', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config: config }),
fetch(`/game/${GAME_SLUG}/scene-objects/${objectId}/quiz-config`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ config }),
})
.then(function (r) { return r.json(); })
.then(function (data) {
.then((r) => r.json())
.then((data) => {
if (fullscreenChanged) { window.location.reload(); return; }
var el = document.getElementById('el-' + objectId);
var widget = el && el.querySelector('.quizBoxWidget');
const el = document.getElementById(`el-${objectId}`);
const widget = el && el.querySelector('.quizBoxWidget');
if (widget && data.rendered_html) widget.outerHTML = data.rendered_html;
});
}
function initPersonnageCommandsPanel() {
var body = document.getElementById('personnageCommandsBody');
const 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) {
const objectId = parseInt(body.dataset.elementId, 10);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir'].forEach((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);
['cmd-haut', 'cmd-bas', 'cmd-gauche', 'cmd-droite', 'cmd-interagir', 'cmd-axe', 'cmd-vitesse'].forEach((id) => {
const el = document.getElementById(id);
if (el) el.addEventListener('change', saveSceneObjectCommands);
});
var container = document.getElementById('personnageCommandsExtra');
const container = document.getElementById('personnageCommandsExtra');
// 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
@@ -890,9 +932,9 @@ function initPersonnageCommandsPanel() {
// 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 = [];
let extra = [];
try { extra = JSON.parse(body.dataset.commandsExtra || '[]'); } catch (e) { extra = []; }
extra.forEach(function (e) {
extra.forEach((e) => {
container.appendChild(personnageCommandExtraRowEl(objectId, e.touche, e.animation));
});
}