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
+51
-49
@@ -8,12 +8,12 @@
|
||||
// sans aller-retour serveur — même fonction que dans screen_edit.html,
|
||||
// pour un comportement identique éditeur/jeu.
|
||||
function forgeShowTab(btn) {
|
||||
var groupId = btn.dataset.tabBtn;
|
||||
var targetId = btn.dataset.tabTarget;
|
||||
document.querySelectorAll('[data-tab-btn="' + groupId + '"]').forEach(function (b) {
|
||||
const groupId = btn.dataset.tabBtn;
|
||||
const targetId = btn.dataset.tabTarget;
|
||||
document.querySelectorAll(`[data-tab-btn="${groupId}"]`).forEach((b) => {
|
||||
b.classList.toggle('is-active', b === btn);
|
||||
});
|
||||
document.querySelectorAll('[data-tab-group="' + groupId + '"]').forEach(function (p) {
|
||||
document.querySelectorAll(`[data-tab-group="${groupId}"]`).forEach((p) => {
|
||||
p.style.display = (p.id === targetId) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
@@ -31,8 +31,8 @@ function showScreen(id) {
|
||||
// visible" du même clic...). Aller vers un écran DIFFÉRENT continue de
|
||||
// tout rejouer normalement, comme un vrai premier affichage.
|
||||
if (id === window.currentScreenId) return;
|
||||
document.querySelectorAll('.playScreen').forEach(s => s.style.display = 'none');
|
||||
const el = document.querySelector('.playScreen[data-screen-id="' + id + '"]');
|
||||
document.querySelectorAll('.playScreen').forEach((s) => s.style.display = 'none');
|
||||
const el = document.querySelector(`.playScreen[data-screen-id="${id}"]`);
|
||||
if (el) {
|
||||
el.style.display = 'block';
|
||||
window.currentScreenId = id;
|
||||
@@ -52,15 +52,15 @@ function showScreen(id) {
|
||||
// Déplacement/animation automatiques de tout personnage posé sur cet
|
||||
// écran (voir static/js/play/personnage-controller.js) — n'a d'effet
|
||||
// que sur un écran "jeu_2d" contenant au moins un objet kind="personnage".
|
||||
forgeStartPersonnageControllers(screensData.find(function(s) { return s.id === id; }));
|
||||
forgeStartPersonnageControllers(screensData.find((s) => s.id === id));
|
||||
// Règles "à la collision/dans un périmètre -> action" (voir
|
||||
// static/js/play/collision-rules-controller.js) — démarré juste après
|
||||
// le contrôleur de personnage dont il dépend (position du joueur).
|
||||
forgeStartCollisionRuleControllers(screensData.find(function(s) { return s.id === id; }));
|
||||
forgeStartCollisionRuleControllers(screensData.find((s) => s.id === id));
|
||||
// Déclencheurs D'ÉCRAN "à l'affichage" (voir screen-triggers-controller.js,
|
||||
// screens/rendering/screen_triggers.py) — narration/cinématique sans
|
||||
// objet requis, exécutée à chaque affichage de CET écran.
|
||||
forgeRunScreenTriggers(screensData.find(function(s) { return s.id === id; }));
|
||||
forgeRunScreenTriggers(screensData.find((s) => s.id === id));
|
||||
// Widget "🏆 Score" (voir static/js/play/dialogue-box-controller.js) :
|
||||
// synchronise le compteur affiché sur CET écran avec le total déjà
|
||||
// gagné — sans ça, un score posé sur un écran affiché APRÈS que le
|
||||
@@ -74,28 +74,29 @@ function showScreen(id) {
|
||||
// arrêtée au changement d'écran — un seul Audio actif à la fois (pas un
|
||||
// tableau comme activeTimerTriggerIntervals dans triggers.js, une
|
||||
// musique de fond ne se cumule jamais avec une autre).
|
||||
var currentBackgroundAudio = null;
|
||||
let currentBackgroundAudio = null;
|
||||
|
||||
function runScreenBackgroundMusic(screenId) {
|
||||
if (currentBackgroundAudio) {
|
||||
currentBackgroundAudio.pause();
|
||||
currentBackgroundAudio = null;
|
||||
}
|
||||
var screenData = screensData.find(function(s) { return String(s.id) === String(screenId); });
|
||||
var url = screenData && screenData.background_music_url;
|
||||
const screenData = screensData.find((s) => String(s.id) === String(screenId));
|
||||
const url = screenData && screenData.background_music_url;
|
||||
if (!url) return;
|
||||
currentBackgroundAudio = new Audio(url);
|
||||
currentBackgroundAudio.loop = true;
|
||||
currentBackgroundAudio.play().catch(function() {});
|
||||
currentBackgroundAudio.play().catch(() => {});
|
||||
}
|
||||
|
||||
// Voir le commentaire sur #playFrame dans le <style> de play.html : la
|
||||
// zone de jeu garde toujours la proportion de l'écran affiché, quelle
|
||||
// que soit la forme réelle de la fenêtre du joueur.
|
||||
function applyScreenAspect(aspect) {
|
||||
var parts = String(aspect).split('/');
|
||||
var w = parseFloat(parts[0]) || 9, h = parseFloat(parts[1]) || 16;
|
||||
var frame = document.getElementById('playFrame');
|
||||
const parts = String(aspect).split('/');
|
||||
const w = parseFloat(parts[0]) || 9; const
|
||||
h = parseFloat(parts[1]) || 16;
|
||||
const frame = document.getElementById('playFrame');
|
||||
if (frame) {
|
||||
frame.style.setProperty('--ratio-w', w);
|
||||
frame.style.setProperty('--ratio-h', h);
|
||||
@@ -109,14 +110,14 @@ function applyScreenAspect(aspect) {
|
||||
// et sa taille en ratio fluide (écran "document") À CHAQUE showScreen(),
|
||||
// pas une seule fois au chargement.
|
||||
function applyScreenKind(kind) {
|
||||
var frame = document.getElementById('playFrame');
|
||||
const frame = document.getElementById('playFrame');
|
||||
if (frame) {
|
||||
frame.classList.toggle('sceneMode', kind === 'jeu_2d');
|
||||
}
|
||||
}
|
||||
|
||||
function screenIndexById(id) {
|
||||
return screensData.findIndex(s => s.id === id);
|
||||
return screensData.findIndex((s) => s.id === id);
|
||||
}
|
||||
|
||||
// ---------- Timeline d'animation (menu "Ajouter un élément" → panneau
|
||||
@@ -126,7 +127,7 @@ function screenIndexById(id) {
|
||||
// Deux types de clip : "animate_css" pose une classe de la bibliothèque
|
||||
// Animate.css (voir animate_css_catalog.py), "custom" joue des
|
||||
// images-clés propres au jeu (voir injectCustomKeyframes ci-dessous). ----------
|
||||
var pendingAnimationTimers = [];
|
||||
let pendingAnimationTimers = [];
|
||||
|
||||
function injectCustomKeyframes() {
|
||||
// Construit UNE FOIS toutes les @keyframes "personnalisées" de tous les
|
||||
@@ -134,20 +135,20 @@ function injectCustomKeyframes() {
|
||||
// pose dans une unique balise <style>, plutôt que d'en injecter une par
|
||||
// clip à chaque lecture — un nom de règle unique par clip (customAnim_id)
|
||||
// évite toute collision entre deux clips personnalisés différents.
|
||||
var rules = [];
|
||||
Object.keys(gameData.animations || {}).forEach(function(screenId) {
|
||||
(gameData.animations[screenId] || []).forEach(function(clip) {
|
||||
const rules = [];
|
||||
Object.keys(gameData.animations || {}).forEach((screenId) => {
|
||||
(gameData.animations[screenId] || []).forEach((clip) => {
|
||||
if (clip.kind !== 'custom' || !clip.custom_keyframes) return;
|
||||
var keyframes;
|
||||
let keyframes;
|
||||
try { keyframes = JSON.parse(clip.custom_keyframes); } catch (e) { return; }
|
||||
var steps = keyframes.map(function(k) {
|
||||
var props = Object.keys(k.props || {}).map(function(p) { return p + ':' + k.props[p] + ';'; }).join(' ');
|
||||
return k.percent + '% { ' + props + ' }';
|
||||
const steps = keyframes.map((k) => {
|
||||
const props = Object.keys(k.props || {}).map((p) => `${p}:${k.props[p]};`).join(' ');
|
||||
return `${k.percent}% { ${props} }`;
|
||||
}).join(' ');
|
||||
rules.push('@keyframes customAnim_' + clip.id + ' { ' + steps + ' }');
|
||||
rules.push(`@keyframes customAnim_${clip.id} { ${steps} }`);
|
||||
});
|
||||
});
|
||||
var styleTag = document.getElementById('customAnimStyles');
|
||||
let styleTag = document.getElementById('customAnimStyles');
|
||||
if (!styleTag) {
|
||||
styleTag = document.createElement('style');
|
||||
styleTag.id = 'customAnimStyles';
|
||||
@@ -164,15 +165,16 @@ function applyAnimationClip(clip) {
|
||||
// collectAnimationClips ci-dessous), y compris pour chaque ligne d'un
|
||||
// Répéteur utilisant ce modèle comme gabarit. Un seul querySelector
|
||||
// n'aurait animé que le tout premier exemplaire trouvé dans le DOM.
|
||||
var targets = document.querySelectorAll('[data-anim-target="' + clip.element_id + '"]');
|
||||
const targets = document.querySelectorAll(`[data-anim-target="${clip.element_id}"]`);
|
||||
if (!targets.length) return;
|
||||
var duration = clip.duration || 1, delay = clip.delay || 0;
|
||||
var iterationCount = clip.iteration_count || '1';
|
||||
var infinite = iterationCount === 'infinite';
|
||||
targets.forEach(function(target) {
|
||||
const duration = clip.duration || 1; const
|
||||
delay = clip.delay || 0;
|
||||
const iterationCount = clip.iteration_count || '1';
|
||||
const infinite = iterationCount === 'infinite';
|
||||
targets.forEach((target) => {
|
||||
if (clip.kind === 'custom') {
|
||||
target.style.animation = 'customAnim_' + clip.id + ' ' + duration + 's ' + (clip.easing || 'ease') +
|
||||
' ' + delay + 's ' + iterationCount;
|
||||
target.style.animation = `customAnim_${clip.id} ${duration}s ${clip.easing || 'ease'
|
||||
} ${delay}s ${iterationCount}`;
|
||||
if (!infinite) {
|
||||
target.addEventListener('animationend', function handler() {
|
||||
target.style.animation = '';
|
||||
@@ -186,14 +188,14 @@ function applyAnimationClip(clip) {
|
||||
// ses frames — résolues ICI depuis le personnage ACTUELLEMENT
|
||||
// assigné, gameData.personnage_animations, pas figées au moment où
|
||||
// le clip a été configuré).
|
||||
var spriteData = {};
|
||||
let spriteData = {};
|
||||
try { spriteData = JSON.parse(clip.custom_keyframes || '{}'); } catch (e) { /* invalide : rien à jouer */ }
|
||||
spriteData.loop = infinite;
|
||||
setTimeout(function() { runSpriteAnimation(target, resolveSpriteFrames(clip.element_id, spriteData)); }, delay * 1000);
|
||||
setTimeout(() => { runSpriteAnimation(target, resolveSpriteFrames(clip.element_id, spriteData)); }, delay * 1000);
|
||||
} else {
|
||||
var animClass = 'animate__' + clip.animate_name;
|
||||
target.style.setProperty('--animate-duration', duration + 's');
|
||||
target.style.setProperty('--animate-delay', delay + 's');
|
||||
const animClass = `animate__${clip.animate_name}`;
|
||||
target.style.setProperty('--animate-duration', `${duration}s`);
|
||||
target.style.setProperty('--animate-delay', `${delay}s`);
|
||||
target.style.animationIterationCount = iterationCount;
|
||||
target.classList.add('animate__animated', animClass);
|
||||
if (!infinite) {
|
||||
@@ -215,17 +217,17 @@ function applyAnimationClip(clip) {
|
||||
// modèle ne se jouait jamais quand le modèle est utilisé ailleurs — seul
|
||||
// l'écran-modèle affiché EN TANT QU'ÉCRAN pouvait la jouer.
|
||||
function collectAnimationClips(screenId) {
|
||||
var visited = {};
|
||||
var clips = [];
|
||||
const visited = {};
|
||||
let clips = [];
|
||||
function visit(sid) {
|
||||
if (visited[sid]) return;
|
||||
visited[sid] = true;
|
||||
clips = clips.concat((gameData.animations || {})[String(sid)] || []);
|
||||
var screenData = screensData.find(function(s){ return String(s.id) === String(sid); });
|
||||
const screenData = screensData.find((s) => String(s.id) === String(sid));
|
||||
if (!screenData) return;
|
||||
screenData.elements.forEach(function(el) {
|
||||
screenData.elements.forEach((el) => {
|
||||
if (!el.element_type_id) return;
|
||||
var templateScreenId = (gameData.element_types || {})[String(el.element_type_id)];
|
||||
const templateScreenId = (gameData.element_types || {})[String(el.element_type_id)];
|
||||
if (templateScreenId != null) visit(templateScreenId);
|
||||
});
|
||||
}
|
||||
@@ -239,11 +241,11 @@ function runAnimationTimeline(screenId) {
|
||||
// attente d'un affichage précédent, pour ne jamais empiler des
|
||||
// déclenchements en double si le joueur revient plusieurs fois de
|
||||
// suite sur le même écran avant la fin de sa timeline.
|
||||
pendingAnimationTimers.forEach(function(t) { clearTimeout(t); });
|
||||
pendingAnimationTimers.forEach((t) => { clearTimeout(t); });
|
||||
pendingAnimationTimers = [];
|
||||
var clips = collectAnimationClips(screenId);
|
||||
clips.forEach(function(clip) {
|
||||
var t = setTimeout(function() { applyAnimationClip(clip); }, (clip.start_time || 0) * 1000);
|
||||
const clips = collectAnimationClips(screenId);
|
||||
clips.forEach((clip) => {
|
||||
const t = setTimeout(() => { applyAnimationClip(clip); }, (clip.start_time || 0) * 1000);
|
||||
pendingAnimationTimers.push(t);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user