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:
co-authored by
Claude Sonnet 5
parent
7db4803b93
commit
c57420c8c9
+281
-239
@@ -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, '"') + '"></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, '"')}"></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));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user