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
+86 -51
View File
@@ -11,17 +11,17 @@
// mécanique que "Modifier un objet de scène"/"Jouer une animation",
// jamais dupliquée.
var activePersonnageControllerInterval = null;
var activePersonnageInteractListener = null;
let activePersonnageControllerInterval = null;
let activePersonnageInteractListener = null;
// Un seul rythme pour TOUTES les poses (marche, idle, interagir, touches
// supplémentaires) — les désynchroniser (ex. idle plus lent que walk)
// n'a aucune justification et se voit immédiatement au jeu (bug signalé
// par l'utilisateur).
var FORGE_PERSONNAGE_FPS = 8;
const FORGE_PERSONNAGE_FPS = 8;
function forgePersonnageAnimExists(elementId, animName) {
var byAnim = (gameData.personnage_animations || {})[elementId];
const byAnim = (gameData.personnage_animations || {})[elementId];
return !!(byAnim && byAnim[animName] && byAnim[animName].length);
}
@@ -48,15 +48,16 @@ function forgePersonnageIdleAnim(elementId) {
// borne un déplacement au monde entier, pas seulement au petit cadre
// visible de la scène.
function forgeUpdateSceneWorldSize(screenData) {
var width = screenData.scene_width, height = screenData.scene_height;
(screenData.elements || []).forEach(function (o) {
let width = screenData.scene_width; let
height = screenData.scene_height;
(screenData.elements || []).forEach((o) => {
if (o.kind !== 'fond') return;
width = Math.max(width, (o.x || 0) + (o.width || 0));
height = Math.max(height, (o.y || 0) + (o.height || 0));
});
screenData.world_width = width;
screenData.world_height = height;
return { width: width, height: height };
return { width, height };
}
// Caméra qui suit le personnage (le premier objet kind="personnage"
@@ -68,22 +69,32 @@ function forgeUpdateSceneWorldSize(screenData) {
// dépasse pas la scène — c'est ce qui garde le comportement identique à
// avant pour tout jeu sans fond plus grand que sa scène.
function forgeUpdateSceneCamera(screenData) {
var worldEl = document.querySelector('.playScreen[data-screen-id="' + screenData.id + '"] .sceneWorld');
const worldEl = document.querySelector(`.playScreen[data-screen-id="${screenData.id}"] .sceneWorld`);
if (!worldEl) return;
var world = forgeUpdateSceneWorldSize(screenData);
worldEl.style.width = world.width + 'px';
worldEl.style.height = world.height + 'px';
const world = forgeUpdateSceneWorldSize(screenData);
worldEl.style.width = `${world.width}px`;
worldEl.style.height = `${world.height}px`;
if (world.width <= screenData.scene_width && world.height <= screenData.scene_height) {
worldEl.style.transform = '';
return;
}
var player = (screenData.elements || []).find(function (o) { return o.kind === 'personnage' && o.personnage_role === 'joueur'; });
var playerEl = player && document.querySelector('[data-object-id="' + player.id + '"]');
var px = playerEl ? (parseFloat(playerEl.style.left) || 0) + (parseFloat(playerEl.style.width) || 0) / 2 : world.width / 2;
var py = playerEl ? (parseFloat(playerEl.style.top) || 0) + (parseFloat(playerEl.style.height) || 0) / 2 : world.height / 2;
var offsetX = Math.min(Math.max(px - screenData.scene_width / 2, 0), Math.max(0, world.width - screenData.scene_width));
var offsetY = Math.min(Math.max(py - screenData.scene_height / 2, 0), Math.max(0, world.height - screenData.scene_height));
worldEl.style.transform = 'translate(' + (-offsetX) + 'px, ' + (-offsetY) + 'px)';
const player = (screenData.elements || []).find((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
const playerEl = player && document.querySelector(`[data-object-id="${player.id}"]`);
const px = playerEl
? (parseFloat(playerEl.style.left) || 0) + (parseFloat(playerEl.style.width) || 0) / 2
: world.width / 2;
const py = playerEl
? (parseFloat(playerEl.style.top) || 0) + (parseFloat(playerEl.style.height) || 0) / 2
: world.height / 2;
const offsetX = Math.min(
Math.max(px - screenData.scene_width / 2, 0),
Math.max(0, world.width - screenData.scene_width),
);
const offsetY = Math.min(
Math.max(py - screenData.scene_height / 2, 0),
Math.max(0, world.height - screenData.scene_height),
);
worldEl.style.transform = `translate(${-offsetX}px, ${-offsetY}px)`;
}
function forgeStopPersonnageControllers() {
@@ -103,7 +114,9 @@ function forgeStopPersonnageControllers() {
// (conditions.js, déjà chargé avant ce fichier dans templates/play.html),
// la MÊME formule que la condition de collision de flow (elementsOverlap).
function forgeObjectCollisionRectAt(left, top, width, height, collision) {
return forgeCollisionRectFromBox({ left: left, top: top, width: width, height: height }, collision);
return forgeCollisionRectFromBox({
left, top, width, height,
}, collision);
}
// Tout objet de scène potentiellement "solide" pour le déplacement au
@@ -112,9 +125,7 @@ function forgeObjectCollisionRectAt(left, top, width, height, collision) {
// objet dont la collision est explicitement désactivée, ni l'objet qui
// se déplace lui-même.
function forgeSolidObstacles(screenData, movingId) {
return (screenData.elements || []).filter(function (o) {
return o.id !== movingId && o.kind !== 'fond' && o.kind !== 'dialogue_box' && o.kind !== 'quiz_box' && o.kind !== 'score_widget' && !(o.collision && o.collision.enabled === false);
});
return (screenData.elements || []).filter((o) => o.id !== movingId && o.kind !== 'fond' && o.kind !== 'dialogue_box' && o.kind !== 'quiz_box' && o.kind !== 'score_widget' && !(o.collision && o.collision.enabled === false));
}
// Est-ce que le personnage, À la position candidate (newLeft/newTop),
@@ -130,7 +141,7 @@ function forgeWouldCollide(screenData, movingObj, newLeft, newTop, width, height
const obstacles = forgeSolidObstacles(screenData, movingObj.id);
for (let i = 0; i < obstacles.length; i++) {
const other = obstacles[i];
const otherEl = document.querySelector('[data-object-id="' + other.id + '"]');
const otherEl = document.querySelector(`[data-object-id="${other.id}"]`);
if (!otherEl) continue;
const otherLeft = parseFloat(otherEl.style.left) || 0;
const otherTop = parseFloat(otherEl.style.top) || 0;
@@ -146,16 +157,17 @@ function forgeWouldCollide(screenData, movingObj, newLeft, newTop, width, height
// (triggers.js), pour un déplacement perçu comme continu.
function forgeRunPersonnageControllerTick(screenData) {
forgeUpdateSceneCamera(screenData);
(screenData.elements || []).forEach(function (obj) {
(screenData.elements || []).forEach((obj) => {
// Seul le personnage "joueur" (voir screens/rendering/personnage_role.py)
// est déplacé/animé au clavier — "ennemie"/"pnj" restent immobiles tant
// qu'aucune logique de flow ne les pilote (voir add_scene_object.py).
if (obj.kind !== 'personnage' || obj.personnage_role !== 'joueur' || !obj.personnage_commandes) return;
var cmd = obj.personnage_commandes;
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
const cmd = obj.personnage_commandes;
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
if (!targetEl) return;
var rawDx = 0, rawDy = 0;
let rawDx = 0; let
rawDy = 0;
if (cmd.axe !== 'vertical') {
if (heldKeys.has(cmd.gauche)) rawDx -= cmd.vitesse;
if (heldKeys.has(cmd.droite)) rawDx += cmd.vitesse;
@@ -168,17 +180,18 @@ function forgeRunPersonnageControllerTick(screenData) {
// Bloque le déplacement axe par axe (pas les deux à la fois dès qu'un
// seul est gêné) — glisser le long d'un mur en diagonale reste
// possible, plus naturel qu'un blocage total au moindre contact.
var curLeft = parseFloat(targetEl.style.left) || 0;
var curTop = parseFloat(targetEl.style.top) || 0;
var boxWidth = parseFloat(targetEl.style.width) || 0;
var boxHeight = parseFloat(targetEl.style.height) || 0;
var dx = rawDx, dy = rawDy;
const curLeft = parseFloat(targetEl.style.left) || 0;
const curTop = parseFloat(targetEl.style.top) || 0;
const boxWidth = parseFloat(targetEl.style.width) || 0;
const boxHeight = parseFloat(targetEl.style.height) || 0;
let dx = rawDx; let
dy = rawDy;
if (dx !== 0) {
var candidateLeft = clampSceneObjectPosition(targetEl, 'left', curLeft + dx);
const candidateLeft = clampSceneObjectPosition(targetEl, 'left', curLeft + dx);
if (forgeWouldCollide(screenData, obj, candidateLeft, curTop, boxWidth, boxHeight)) dx = 0;
}
if (dy !== 0) {
var candidateTop = clampSceneObjectPosition(targetEl, 'top', curTop + dy);
const candidateTop = clampSceneObjectPosition(targetEl, 'top', curTop + dy);
if (forgeWouldCollide(screenData, obj, curLeft, candidateTop, boxWidth, boxHeight)) dy = 0;
}
// Se tourne vers la direction TENTÉE même si le déplacement est
@@ -191,14 +204,24 @@ function forgeRunPersonnageControllerTick(screenData) {
// animations sont lentes à côté de la marche" — aucune raison réelle
// de les désynchroniser).
if (dx === 0 && dy === 0) {
var idleAnim = forgePersonnageIdleAnim(obj.id);
if (idleAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: idleAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }));
const idleAnim = forgePersonnageIdleAnim(obj.id);
if (idleAnim) {
runSpriteAnimation(
targetEl,
resolveSpriteFrames(obj.id, { animation: idleAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }),
);
}
return;
}
if (dx !== 0) applyObjectProperty(targetEl, 'pos_x_relatif', dx);
if (dy !== 0) applyObjectProperty(targetEl, 'pos_y_relatif', dy);
var moveAnim = forgePersonnageMovementAnim(obj.id);
if (moveAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: moveAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }));
const moveAnim = forgePersonnageMovementAnim(obj.id);
if (moveAnim) {
runSpriteAnimation(
targetEl,
resolveSpriteFrames(obj.id, { animation: moveAnim, fps: FORGE_PERSONNAGE_FPS, loop: true }),
);
}
});
}
@@ -209,7 +232,7 @@ function forgeRunPersonnageControllerTick(screenData) {
// (sauter, attaquer, courir...) sans ajouter un champ fixe par pose.
function forgePersonnageActionAnim(cmd, key) {
if (cmd.interagir === key) return 'interact';
var extraMatch = (cmd.extra || []).find(function (e) { return e.touche === key; });
const extraMatch = (cmd.extra || []).find((e) => e.touche === key);
return extraMatch ? extraMatch.animation : null;
}
@@ -218,13 +241,16 @@ function forgePersonnageActionAnim(cmd, key) {
// attaquer...), jamais un mouvement continu.
function forgeStartPersonnageInteractListener(screenData) {
activePersonnageInteractListener = function (ev) {
(screenData.elements || []).forEach(function (obj) {
(screenData.elements || []).forEach((obj) => {
if (obj.kind !== 'personnage' || obj.personnage_role !== 'joueur' || !obj.personnage_commandes) return;
var animName = forgePersonnageActionAnim(obj.personnage_commandes, ev.key);
const animName = forgePersonnageActionAnim(obj.personnage_commandes, ev.key);
if (!animName || !forgePersonnageAnimExists(obj.id, animName)) return;
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
if (!targetEl) return;
runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: animName, fps: FORGE_PERSONNAGE_FPS, loop: false }));
runSpriteAnimation(
targetEl,
resolveSpriteFrames(obj.id, { animation: animName, fps: FORGE_PERSONNAGE_FPS, loop: false }),
);
});
};
window.addEventListener('keydown', activePersonnageInteractListener);
@@ -236,9 +262,9 @@ function forgeStartPersonnageInteractListener(screenData) {
function forgeStartPersonnageControllers(screenData) {
forgeStopPersonnageControllers();
if (!screenData || screenData.kind !== 'jeu_2d') return;
var elements = screenData.elements || [];
var hasPersonnage = elements.some(function (o) { return o.kind === 'personnage' && o.personnage_role === 'joueur'; });
var hasFond = elements.some(function (o) { return o.kind === 'fond'; });
const elements = screenData.elements || [];
const hasPersonnage = elements.some((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
const hasFond = elements.some((o) => o.kind === 'fond');
// Appliqué tout de suite (avant le premier tick de l'intervalle, 50ms
// plus tard) pour éviter un flash d'une frame à la taille/caméra par
// défaut. Un fond seul (sans personnage) n'a besoin que de CET appel
@@ -246,7 +272,7 @@ function forgeStartPersonnageControllers(screenData) {
// tick régulier pour le déplacement continu au clavier.
if (hasFond) forgeUpdateSceneCamera(screenData);
if (!hasPersonnage) return;
activePersonnageControllerInterval = setInterval(function () { forgeRunPersonnageControllerTick(screenData); }, 50);
activePersonnageControllerInterval = setInterval(() => { forgeRunPersonnageControllerTick(screenData); }, 50);
forgeStartPersonnageInteractListener(screenData);
}
@@ -255,10 +281,19 @@ function forgeStartPersonnageControllers(screenData) {
// client.
if (typeof module !== 'undefined' && module.exports) {
module.exports = {
forgePersonnageAnimExists, forgePersonnageMovementAnim, forgePersonnageIdleAnim, forgePersonnageActionAnim,
forgeRunPersonnageControllerTick, forgeStartPersonnageControllers, forgeStopPersonnageControllers,
forgeStartPersonnageInteractListener, forgeUpdateSceneWorldSize, forgeUpdateSceneCamera,
forgeObjectCollisionRectAt, forgeSolidObstacles, forgeWouldCollide,
forgePersonnageAnimExists,
forgePersonnageMovementAnim,
forgePersonnageIdleAnim,
forgePersonnageActionAnim,
forgeRunPersonnageControllerTick,
forgeStartPersonnageControllers,
forgeStopPersonnageControllers,
forgeStartPersonnageInteractListener,
forgeUpdateSceneWorldSize,
forgeUpdateSceneCamera,
forgeObjectCollisionRectAt,
forgeSolidObstacles,
forgeWouldCollide,
};
// node:test charge ce fichier isolément (voir __tests__/) — jamais un
// <script> séparé en navigateur, où conditions.js est déjà chargé avant