Lot 4 modernisation JS (SonarLint) + restauration CI sonarqube non-bloquante
Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s
Build and deploy / test-python (push) Successful in 11m18s
Build and deploy / test-js (push) Successful in 1m16s
Build and deploy / lint-python (push) Successful in 4m24s
Build and deploy / lint-js (push) Successful in 2m51s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Successful in 5m30s
Corrige les findings SonarQube (via SonarLint IDE, fichier par fichier) sur ~24 fichiers static/js/ : parseFloat/parseInt -> Number.*, .replace(/x/g,y) -> .replaceAll, .indexOf() -> .includes()/.startsWith(), getAttribute/setAttribute -> .dataset, tableaux -> Set, x && x.y -> x?.y (verifie site par site), extraction de template litteraux imbriques, ternaires imbriquees, refactors de complexite cognitive (S3776) via tables de dispatch, Object.hasOwn, .at(), et deduplication de fonctions identiques (S4144). Deux exceptions S2486 documentees/corrigees (filter-repeater-rows.js) et un cas S2703 de partage inter-scripts complete (_collisionWizard, trigger-editor.js <-> collision-rules-editor.js). Details complets dans CODE_QUALITY.md section 5. Restaure aussi le job CI "sonarqube" (non-bloquant) dans .gitea/workflows/deploy.yml maintenant que l'instance prod est operationnelle. Suites vertes : 276/276 JS (node --test), 591/591 Python (pytest). SKIP=djlint sur ce commit : hook djlint bloquant sur le backlog H021 (styles inline, 49 occurrences/6 templates) deja documente comme dette assumee non traitee dans CODE_QUALITY.md section 6, aucun rapport avec ce commit (aucun template touche ici) - valide explicitement avec l'utilisateur avant de contourner ce hook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
66d8eaeae8
commit
ccf836f2c5
@@ -25,10 +25,10 @@ function forgeCollisionRuleStateKey(objectId, ruleIndex) {
|
||||
}
|
||||
|
||||
function forgeElementBoxRect(el, collision) {
|
||||
const left = parseFloat(el.style.left) || 0;
|
||||
const top = parseFloat(el.style.top) || 0;
|
||||
const width = parseFloat(el.style.width) || 0;
|
||||
const height = parseFloat(el.style.height) || 0;
|
||||
const left = Number.parseFloat(el.style.left) || 0;
|
||||
const top = Number.parseFloat(el.style.top) || 0;
|
||||
const width = Number.parseFloat(el.style.width) || 0;
|
||||
const height = Number.parseFloat(el.style.height) || 0;
|
||||
return forgeCollisionRectFromBox({
|
||||
left, top, width, height,
|
||||
}, collision);
|
||||
@@ -86,7 +86,7 @@ function forgeCollisionInteractBubbleEl(objectId, parentEl) {
|
||||
if (!bubble) {
|
||||
bubble = document.createElement('div');
|
||||
bubble.className = 'collisionInteractBubble';
|
||||
bubble.setAttribute('data-for-object-id', objectId);
|
||||
bubble.dataset.forObjectId = objectId;
|
||||
parentEl.appendChild(bubble);
|
||||
}
|
||||
return bubble;
|
||||
@@ -112,8 +112,8 @@ function forgeShowCollisionInteractBubble(objectEl, objectId, objRect, key) {
|
||||
}
|
||||
|
||||
function forgeHideCollisionInteractBubble(objectId, objectEl) {
|
||||
const parentEl = objectEl && objectEl.parentElement;
|
||||
const bubble = parentEl && parentEl.querySelector(`.collisionInteractBubble[data-for-object-id="${objectId}"]`);
|
||||
const parentEl = objectEl?.parentElement;
|
||||
const bubble = parentEl?.querySelector(`.collisionInteractBubble[data-for-object-id="${objectId}"]`);
|
||||
if (bubble) bubble.style.display = 'none';
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ function forgeIndicationBubbleEl(objectId, parentEl) {
|
||||
if (!bubble) {
|
||||
bubble = document.createElement('div');
|
||||
bubble.className = 'collisionIndicationBubble';
|
||||
bubble.setAttribute('data-for-object-id', objectId);
|
||||
bubble.dataset.forObjectId = objectId;
|
||||
parentEl.appendChild(bubble);
|
||||
}
|
||||
return bubble;
|
||||
@@ -139,7 +139,7 @@ function forgeIndicationBubbleEl(objectId, parentEl) {
|
||||
|
||||
function forgeShowIndicationBubble(objectId, texte, dureeMs) {
|
||||
const targetEl = document.querySelector(`[data-object-id="${objectId}"]`);
|
||||
const parentEl = targetEl && targetEl.parentElement;
|
||||
const parentEl = targetEl?.parentElement;
|
||||
if (!targetEl || !parentEl || !texte) return;
|
||||
const objRect = forgeElementBoxRect(targetEl);
|
||||
const bubble = forgeIndicationBubbleEl(objectId, parentEl);
|
||||
@@ -153,85 +153,114 @@ function forgeShowIndicationBubble(objectId, texte, dureeMs) {
|
||||
}, dureeMs || FORGE_INDICATION_DEFAULT_DURATION_MS);
|
||||
}
|
||||
|
||||
// Affiche LE DIALOGUE DE CETTE ACTION PRÉCISE (action.dialogue.lines —
|
||||
// chaque occurrence de "Déclencher ce dialogue" porte le sien, voir
|
||||
// screens/rendering/collision_rules.py) dans le widget "💬 Boîte de
|
||||
// dialogue" posé sur l'écran (voir static/js/play/dialogue-box-
|
||||
// controller.js) — sans effet si aucun widget n'est posé, comme le reste
|
||||
// de ce moteur. Le nom affiché (en-tête du quiz, xAPI) est celui de
|
||||
// l'objet déclencheur (voir "ℹ️ Informations") — plus de "quête" pour le
|
||||
// porter. mark_completed (optionnel) : marque ce dialogue terminé une
|
||||
// fois épuisé. `action.then` (chaînage, voir maquette validée : "pouvoir
|
||||
// modifier un déclencheur pour lui ajouter des actions") NE PEUT PAS
|
||||
// s'exécuter immédiatement — un dialogue attend des clics du joueur —
|
||||
// transmis à forgeShowDialogueBox, exécuté par dialogue-box-controller.js
|
||||
// une fois le dialogue épuisé.
|
||||
function runDialogueCollisionAction(action, targetEl, obj) {
|
||||
const lines = action.dialogue?.lines || [];
|
||||
const dialogueId = action.dialogue?.id;
|
||||
forgeShowDialogueBox(dialogueId, obj.name || 'PNJ', lines, action.mark_completed, action.then);
|
||||
}
|
||||
|
||||
// Modifie une variable globale — même vocabulaire/calcul que l'action
|
||||
// "Modifier une variable" du graphe de logique document (voir
|
||||
// screens/data_actions/apply_variable_action.py), portée ici par
|
||||
// forgeApplyVariableActionOffline (static/js/play/offline/apply-actions.js,
|
||||
// TOUJOURS chargée — voir templates/play.html — pas seulement en export,
|
||||
// donc aucun branchement FORGE_OFFLINE nécessaire, même philosophie que
|
||||
// le reste de ce fichier). Instantané : `then` s'exécute tout de suite,
|
||||
// contrairement à un dialogue.
|
||||
function runVariableCollisionAction(action, targetEl, obj) {
|
||||
forgeApplyVariableActionOffline(gameData, action);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}
|
||||
|
||||
// Réutilise evaluateConditionClause (static/js/play/conditions.js), même
|
||||
// comparaison qu'un nœud Condition de l'éditeur de logique, sur une
|
||||
// VARIABLE GLOBALE (gameData.variables) — jamais un champ d'objet ici
|
||||
// (voir maquette validée : le losange ne teste qu'une variable).
|
||||
function runConditionCollisionAction(action, targetEl, obj) {
|
||||
const isTrue = evaluateConditionClause({
|
||||
source: 'variable', variable: action.variable, operator: action.operateur, value: action.valeur,
|
||||
});
|
||||
const branch = isTrue ? action.si_vrai : action.si_faux;
|
||||
if (branch) forgeRunCollisionRuleAction(branch, targetEl, obj);
|
||||
}
|
||||
|
||||
// Cible N'IMPORTE QUEL objet de l'écran (action.object_id, pas forcément
|
||||
// celui qui porte le déclencheur) — réutilise TEL QUEL applyObjectProperty
|
||||
// (static/js/play/actions.js, équivalent scène 2D d'applyElementProperty),
|
||||
// plutôt que de dupliquer cette logique. Partagée entre "surbrillance" et
|
||||
// "visibilite", qui suivent exactement le même patron.
|
||||
function runSurbrillanceOuVisibiliteCollisionAction(action, targetEl, obj) {
|
||||
const targetObjEl = document.querySelector(`[data-object-id="${action.object_id}"]`);
|
||||
if (targetObjEl) applyObjectProperty(targetObjEl, action.type, action.valeur);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}
|
||||
|
||||
// Effet sonore ponctuel, non bouclé — même comportement que l'action
|
||||
// "Jouer un son" du Flow (static/js/play/actions.js), instantané : `then`
|
||||
// s'exécute tout de suite, sans attendre la fin du son.
|
||||
function runSonCollisionAction(action, targetEl, obj) {
|
||||
if (action.asset_url) new Audio(action.asset_url).play().catch(() => {});
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}
|
||||
|
||||
// Bloque la suite de la chaîne "then" jusqu'à la fin de la vidéo (ou
|
||||
// "Passer") — voir static/js/play/video-overlay.js, même esprit qu'un
|
||||
// dialogue ci-dessus.
|
||||
function runVideoCollisionAction(action, targetEl, obj) {
|
||||
forgeShowVideoOverlay(action.asset_url, action.mode, () => {
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
});
|
||||
}
|
||||
|
||||
function runIndicationCollisionAction(action, targetEl, obj) {
|
||||
forgeShowIndicationBubble(action.object_id, action.texte, action.duree_ms);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}
|
||||
|
||||
// Suspend la chaîne "then" pendant data_value SECONDES — même vocabulaire
|
||||
// que l'action "attendre" du Flow (static/js/play/actions.js), utile ex.
|
||||
// pour laisser un temps de lecture après une narration à l'affichage de
|
||||
// l'écran.
|
||||
function runAttendreCollisionAction(action, targetEl, obj) {
|
||||
const seconds = Number.parseFloat(action.data_value);
|
||||
const ms = Number.isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||||
setTimeout(() => {
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}, ms);
|
||||
}
|
||||
|
||||
const COLLISION_RULE_ACTION_HANDLERS = {
|
||||
dialogue: runDialogueCollisionAction,
|
||||
variable: runVariableCollisionAction,
|
||||
condition: runConditionCollisionAction,
|
||||
surbrillance: runSurbrillanceOuVisibiliteCollisionAction,
|
||||
visibilite: runSurbrillanceOuVisibiliteCollisionAction,
|
||||
son: runSonCollisionAction,
|
||||
video: runVideoCollisionAction,
|
||||
indication: runIndicationCollisionAction,
|
||||
attendre: runAttendreCollisionAction,
|
||||
};
|
||||
|
||||
// Exécute une action (top-level, sub_action d'un "interagir", ou branche
|
||||
// si_vrai/si_faux d'une "condition") — jamais appelée pour un "interagir"
|
||||
// lui-même à ce niveau, voir forgeCollisionRuleTick qui le gère à part
|
||||
// (affichage de bulle + attente de touche).
|
||||
function forgeRunCollisionRuleAction(action, targetEl, obj) {
|
||||
if (action.type === 'dialogue') {
|
||||
// Affiche LE DIALOGUE DE CETTE ACTION PRÉCISE (action.dialogue.lines
|
||||
// — chaque occurrence de "Déclencher ce dialogue" porte le sien,
|
||||
// voir screens/rendering/collision_rules.py) dans le widget "💬 Boîte
|
||||
// de dialogue" posé sur l'écran (voir static/js/play/
|
||||
// dialogue-box-controller.js) — sans effet si aucun widget n'est
|
||||
// posé, comme le reste de ce moteur. Le nom affiché (en-tête du
|
||||
// quiz, xAPI) est celui de l'objet déclencheur (voir "ℹ️
|
||||
// Informations") — plus de "quête" pour le porter. mark_completed
|
||||
// (optionnel) : marque ce dialogue terminé une fois épuisé.
|
||||
// `action.then` (chaînage, voir maquette validée : "pouvoir modifier
|
||||
// un déclencheur pour lui ajouter des actions") NE PEUT PAS
|
||||
// s'exécuter immédiatement — un dialogue attend des clics du joueur
|
||||
// — transmis à forgeShowDialogueBox, exécuté par dialogue-box-
|
||||
// controller.js une fois le dialogue épuisé.
|
||||
const lines = (action.dialogue && action.dialogue.lines) || [];
|
||||
const dialogueId = action.dialogue && action.dialogue.id;
|
||||
forgeShowDialogueBox(dialogueId, obj.name || 'PNJ', lines, action.mark_completed, action.then);
|
||||
} else if (action.type === 'variable') {
|
||||
// Modifie une variable globale — même vocabulaire/calcul que l'action
|
||||
// "Modifier une variable" du graphe de logique document (voir
|
||||
// screens/data_actions/apply_variable_action.py), portée ici par
|
||||
// forgeApplyVariableActionOffline (static/js/play/offline/apply-actions.js,
|
||||
// TOUJOURS chargée — voir templates/play.html — pas seulement en
|
||||
// export, donc aucun branchement FORGE_OFFLINE nécessaire, même
|
||||
// philosophie que le reste de ce fichier). Instantané : `then`
|
||||
// s'exécute tout de suite, contrairement à un dialogue.
|
||||
forgeApplyVariableActionOffline(gameData, action);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
} else if (action.type === 'condition') {
|
||||
// Réutilise evaluateConditionClause (static/js/play/conditions.js),
|
||||
// même comparaison qu'un nœud Condition de l'éditeur de logique, sur
|
||||
// une VARIABLE GLOBALE (gameData.variables) — jamais un champ d'objet
|
||||
// ici (voir maquette validée : le losange ne teste qu'une variable).
|
||||
const isTrue = evaluateConditionClause({
|
||||
source: 'variable', variable: action.variable, operator: action.operateur, value: action.valeur,
|
||||
});
|
||||
const branch = isTrue ? action.si_vrai : action.si_faux;
|
||||
if (branch) forgeRunCollisionRuleAction(branch, targetEl, obj);
|
||||
} else if (action.type === 'surbrillance' || action.type === 'visibilite') {
|
||||
// Cible N'IMPORTE QUEL objet de l'écran (action.object_id, pas
|
||||
// forcément celui qui porte le déclencheur) — réutilise TEL QUEL
|
||||
// applyObjectProperty (static/js/play/actions.js, équivalent scène 2D
|
||||
// d'applyElementProperty), plutôt que de dupliquer cette logique.
|
||||
const targetObjEl = document.querySelector(`[data-object-id="${action.object_id}"]`);
|
||||
if (targetObjEl) applyObjectProperty(targetObjEl, action.type, action.valeur);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
} else if (action.type === 'son') {
|
||||
// Effet sonore ponctuel, non bouclé — même comportement que l'action
|
||||
// "Jouer un son" du Flow (static/js/play/actions.js), instantané :
|
||||
// `then` s'exécute tout de suite, sans attendre la fin du son.
|
||||
if (action.asset_url) new Audio(action.asset_url).play().catch(() => {});
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
} else if (action.type === 'video') {
|
||||
// Bloque la suite de la chaîne "then" jusqu'à la fin de la vidéo (ou
|
||||
// "Passer") — voir static/js/play/video-overlay.js, même esprit
|
||||
// qu'un dialogue ci-dessus.
|
||||
forgeShowVideoOverlay(action.asset_url, action.mode, () => {
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
});
|
||||
} else if (action.type === 'indication') {
|
||||
forgeShowIndicationBubble(action.object_id, action.texte, action.duree_ms);
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
} else if (action.type === 'attendre') {
|
||||
// Suspend la chaîne "then" pendant data_value SECONDES — même
|
||||
// vocabulaire que l'action "attendre" du Flow (static/js/play/
|
||||
// actions.js), utile ex. pour laisser un temps de lecture après une
|
||||
// narration à l'affichage de l'écran.
|
||||
const seconds = parseFloat(action.data_value);
|
||||
const ms = Number.isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||||
setTimeout(() => {
|
||||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||||
}, ms);
|
||||
}
|
||||
const handler = COLLISION_RULE_ACTION_HANDLERS[action.type];
|
||||
if (handler) handler(action, targetEl, obj);
|
||||
}
|
||||
|
||||
function forgeCollisionRuleTick(screenData) {
|
||||
@@ -242,7 +271,7 @@ function forgeCollisionRuleTick(screenData) {
|
||||
|
||||
(screenData.elements || []).forEach((obj) => {
|
||||
const rules = obj.collision_rules;
|
||||
if (!rules || !rules.length) return;
|
||||
if (!rules?.length) return;
|
||||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||||
if (!targetEl) return;
|
||||
const objRect = forgeElementBoxRect(targetEl, obj.collision);
|
||||
@@ -261,7 +290,7 @@ function forgeCollisionRuleTick(screenData) {
|
||||
if (rule.action.type === 'interagir') {
|
||||
if (isActive) {
|
||||
anyInteractActive = true;
|
||||
forgeShowCollisionInteractBubble(targetEl, obj.id, objRect, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E');
|
||||
forgeShowCollisionInteractBubble(targetEl, obj.id, objRect, player.personnage_commandes?.interagir || 'E');
|
||||
}
|
||||
forgeCollisionRuleActiveState[stateKey] = isActive;
|
||||
return;
|
||||
@@ -280,10 +309,10 @@ function forgeCollisionRuleTick(screenData) {
|
||||
function forgeStartCollisionRuleInteractListener(screenData) {
|
||||
activeCollisionRuleInteractListener = function (ev) {
|
||||
const player = (screenData.elements || []).find((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
|
||||
if (!player || !player.personnage_commandes || ev.key !== player.personnage_commandes.interagir) return;
|
||||
if (!player?.personnage_commandes || ev.key !== player.personnage_commandes.interagir) return;
|
||||
(screenData.elements || []).forEach((obj) => {
|
||||
const rules = obj.collision_rules;
|
||||
if (!rules || !rules.length) return;
|
||||
if (!rules?.length) return;
|
||||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||||
if (!targetEl) return;
|
||||
rules.forEach((rule, ruleIndex) => {
|
||||
@@ -314,7 +343,7 @@ function forgeBindClicSurvolRules(screenData) {
|
||||
forgeUnbindClicSurvolRules();
|
||||
(screenData.elements || []).forEach((obj) => {
|
||||
const rules = obj.collision_rules;
|
||||
if (!rules || !rules.length) return;
|
||||
if (!rules?.length) return;
|
||||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||||
if (!targetEl) return;
|
||||
rules.forEach((rule) => {
|
||||
@@ -347,11 +376,11 @@ function forgeStopCollisionRuleControllers() {
|
||||
// ET au moins un objet avec des règles.
|
||||
function forgeStartCollisionRuleControllers(screenData) {
|
||||
forgeStopCollisionRuleControllers();
|
||||
if (!screenData || screenData.kind !== 'jeu_2d') return;
|
||||
if (screenData?.kind !== 'jeu_2d') return;
|
||||
const elements = screenData.elements || [];
|
||||
forgeBindClicSurvolRules(screenData);
|
||||
const hasPlayer = elements.some((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
|
||||
const hasRules = elements.some((o) => o.collision_rules && o.collision_rules.length);
|
||||
const hasRules = elements.some((o) => o.collision_rules?.length);
|
||||
if (!hasPlayer || !hasRules) return;
|
||||
activeCollisionRuleInterval = setInterval(() => { forgeCollisionRuleTick(screenData); }, 50);
|
||||
forgeStartCollisionRuleInteractListener(screenData);
|
||||
|
||||
Reference in New Issue
Block a user