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
@@ -9,30 +9,37 @@
|
||||
// exacte d'une règle (côté serveur, où elle est validée une seconde fois
|
||||
// avant d'être enregistrée).
|
||||
|
||||
var COLLISION_ACTION_LABELS = {
|
||||
dialogue: 'Déclencher ce dialogue', variable: 'Modifier une variable',
|
||||
surbrillance: 'Mettre en surbrillance', visibilite: 'Rendre visible/invisible',
|
||||
son: 'Jouer un son', video: 'Jouer une vidéo', indication: 'Afficher une indication',
|
||||
const COLLISION_ACTION_LABELS = {
|
||||
dialogue: 'Déclencher ce dialogue',
|
||||
variable: 'Modifier une variable',
|
||||
surbrillance: 'Mettre en surbrillance',
|
||||
visibilite: 'Rendre visible/invisible',
|
||||
son: 'Jouer un son',
|
||||
video: 'Jouer une vidéo',
|
||||
indication: 'Afficher une indication',
|
||||
attendre: 'Attendre quelques secondes',
|
||||
interagir: 'Interagir avec une touche', condition: 'Si une condition est vraie',
|
||||
interagir: 'Interagir avec une touche',
|
||||
condition: 'Si une condition est vraie',
|
||||
};
|
||||
// Ordre d'affichage — "interagir"/"condition" en dernier (comportements
|
||||
// spéciaux), le reste (les FEUILLES, voir screens/rendering/
|
||||
// collision_rules.py::LEAF_ACTION_TYPES) dans le même ordre que
|
||||
// COLLISION_CHAIN_CHOICES ci-dessous, pour rester cohérent d'un écran à
|
||||
// l'autre de l'assistant.
|
||||
var COLLISION_WIZARD_ACTION_TYPES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre', 'interagir', 'condition'];
|
||||
const COLLISION_WIZARD_ACTION_TYPES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre', 'interagir', 'condition'];
|
||||
// FEUILLES chaînables via "then" — utilisées à la fois pour l'étape
|
||||
// "Ensuite ?" (renderCollisionWizardChainStep) et les branches Vrai/Faux
|
||||
// d'une condition (renderCollisionWizardBranchStep) : MÊME liste des deux
|
||||
// côtés, voir screens/rendering/collision_rules.py::LEAF_ACTION_TYPES.
|
||||
var COLLISION_CHAIN_CHOICES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre'];
|
||||
var COLLISION_TRIGGER_LABELS = { collision: 'À la collision', clic: 'Au clic', survol: 'Au survol', affichage: 'À l\'affichage de l\'écran' };
|
||||
const COLLISION_CHAIN_CHOICES = ['dialogue', 'variable', 'surbrillance', 'visibilite', 'son', 'video', 'indication', 'attendre'];
|
||||
const COLLISION_TRIGGER_LABELS = {
|
||||
collision: 'À la collision', clic: 'Au clic', survol: 'Au survol', affichage: 'À l\'affichage de l\'écran',
|
||||
};
|
||||
// Opérations qui n'ont besoin d'AUCUNE valeur (voir screens/labels/
|
||||
// data_operations.py::DATA_OPERATIONS) — le champ valeur de l'étape
|
||||
// "Modifier une variable" (renderCollisionWizardVariableStep) se masque
|
||||
// pour celles-ci.
|
||||
var COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
|
||||
const COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', 'basculer_bool'];
|
||||
|
||||
// obj.thumbnail_html (screens.render_scene_object) porte un style inline
|
||||
// PENSÉ POUR LE CANEVAS (position:absolute + left/top en dur, voir
|
||||
@@ -46,8 +53,8 @@ var COLLISION_NO_VALUE_OPERATIONS = ['definir_bool_vrai', 'definir_bool_faux', '
|
||||
// (l'image elle-même) est repris. Utilisée par static/js/triggers/
|
||||
// trigger-editor.js (chaîne de bulles + grille de choix d'objet).
|
||||
function collisionRuleThumbHtml(obj) {
|
||||
var match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
|
||||
return match ? '<img src="' + match[1] + '" alt="">' : '';
|
||||
const match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || '');
|
||||
return match ? `<img src="${match[1]}" alt="">` : '';
|
||||
}
|
||||
|
||||
// Id court généré CÔTÉ CLIENT pour une NOUVELLE feuille ("dialogue" ou
|
||||
@@ -58,7 +65,7 @@ function collisionRuleThumbHtml(obj) {
|
||||
// permet d'adresser CETTE feuille précise pour y attacher un "then"
|
||||
// plus tard (voir "+ Ajouter une action", trigger-editor.js).
|
||||
function collisionGenerateActionId() {
|
||||
return 'd_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||
return `d_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`; // NOSONAR S2245 - id local UI, jamais crypto
|
||||
}
|
||||
|
||||
// ---------- Assistant "déclencheur -> action" ----------
|
||||
@@ -68,12 +75,14 @@ function collisionGenerateActionId() {
|
||||
// `action`, ou dans `action.sub_action` au second niveau pour "interagir").
|
||||
// Plus de "quête cible" à connaître d'avance — la règle est posée sur
|
||||
// l'objet, un déclencheur "dialogue" se suffit à lui-même.
|
||||
var _collisionWizard = null;
|
||||
let _collisionWizard = null;
|
||||
|
||||
// Appelée après avoir choisi l'objet dans la modale d'ajout de déclencheur
|
||||
// (voir triggerSelectObject, trigger-editor.js).
|
||||
function openTriggerWizard(objectId, bodyEl) {
|
||||
_collisionWizard = { target: 'object', objectId: objectId, trigger: null, bodyEl: bodyEl };
|
||||
_collisionWizard = {
|
||||
target: 'object', objectId, trigger: null, bodyEl,
|
||||
};
|
||||
renderCollisionWizardTriggerStep();
|
||||
}
|
||||
|
||||
@@ -83,7 +92,9 @@ function openTriggerWizard(objectId, bodyEl) {
|
||||
// un seul déclencheur possible ("affichage"), donc l'étape 1 n'a qu'un
|
||||
// choix, gardée pour rester cohérente avec le parcours objet.
|
||||
function openScreenTriggerWizard(screenId, bodyEl) {
|
||||
_collisionWizard = { target: 'screen', screenId: screenId, trigger: null, bodyEl: bodyEl };
|
||||
_collisionWizard = {
|
||||
target: 'screen', screenId, trigger: null, bodyEl,
|
||||
};
|
||||
renderCollisionWizardTriggerStep();
|
||||
}
|
||||
|
||||
@@ -96,15 +107,12 @@ function collisionWizardBodyEl() {
|
||||
// reste affichée telle quelle même à un seul choix (plutôt que sautée
|
||||
// automatiquement) pour rester cohérente d'un parcours à l'autre.
|
||||
function renderCollisionWizardTriggerStep() {
|
||||
var keys = _collisionWizard.target === 'screen' ? ['affichage'] : ['collision', 'clic', 'survol'];
|
||||
var buttons = keys.map(function (key, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseTrigger(\'' + key + '\')">' +
|
||||
'<span>' + COLLISION_TRIGGER_LABELS[key] + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Cette règle se déclenche...</p>' +
|
||||
'<div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
const keys = _collisionWizard.target === 'screen' ? ['affichage'] : ['collision', 'clic', 'survol'];
|
||||
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseTrigger('${key}')">`
|
||||
+ `<span>${COLLISION_TRIGGER_LABELS[key]}</span>`
|
||||
+ '</button>').join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Cette règle se déclenche...</p>'
|
||||
+ `<div class="collisionWizardChoices">${buttons}</div>`;
|
||||
}
|
||||
|
||||
function collisionWizardChooseTrigger(trigger) {
|
||||
@@ -119,17 +127,14 @@ function collisionWizardChooseTrigger(trigger) {
|
||||
// (bulle "Appuie sur X" tant que le contact dure) — jamais pour "clic"/
|
||||
// "survol"/"affichage", qui n'ont pas cette notion de contact prolongé.
|
||||
function renderCollisionWizardActionStep(nested) {
|
||||
var types = (nested || _collisionWizard.trigger !== 'collision')
|
||||
? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; })
|
||||
const types = (nested || _collisionWizard.trigger !== 'collision')
|
||||
? COLLISION_WIZARD_ACTION_TYPES.filter((t) => t !== 'interagir')
|
||||
: COLLISION_WIZARD_ACTION_TYPES;
|
||||
var buttons = types.map(function (type, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
|
||||
'<span>' + COLLISION_ACTION_LABELS[type] + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">' + (nested ? 'Une fois la touche pressée...' : 'Alors...') + '</p>' +
|
||||
'<div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
const buttons = types.map((type, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseAction('${type}', ${nested ? 'true' : 'false'})">`
|
||||
+ `<span>${COLLISION_ACTION_LABELS[type]}</span>`
|
||||
+ '</button>').join('');
|
||||
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${nested ? 'Une fois la touche pressée...' : 'Alors...'}</p>`
|
||||
+ `<div class="collisionWizardChoices">${buttons}</div>`;
|
||||
}
|
||||
|
||||
function collisionWizardFinalizeAction(action, nested) {
|
||||
@@ -156,17 +161,18 @@ function collisionWizardFinalizeAction(action, nested) {
|
||||
// "liste ENTIÈRE" des deux côtés (recharge les règles existantes, y
|
||||
// ajoute la nouvelle, sauvegarde tout), géré côté serveur.
|
||||
function collisionWizardSaveRule(action) {
|
||||
var rule = { trigger: _collisionWizard.trigger, action: action };
|
||||
const rule = { trigger: _collisionWizard.trigger, action };
|
||||
closeTriggerModal();
|
||||
var isScreen = _collisionWizard.target === 'screen';
|
||||
var url = isScreen
|
||||
? '/game/' + GAME_SLUG + '/screens/' + _collisionWizard.screenId + '/triggers'
|
||||
: '/game/' + GAME_SLUG + '/triggers';
|
||||
var body = isScreen ? { rule: rule } : { object_id: _collisionWizard.objectId, rule: rule };
|
||||
const isScreen = _collisionWizard.target === 'screen';
|
||||
const url = isScreen
|
||||
? `/game/${GAME_SLUG}/screens/${_collisionWizard.screenId}/triggers`
|
||||
: `/game/${GAME_SLUG}/triggers`;
|
||||
const body = isScreen ? { rule } : { object_id: _collisionWizard.objectId, rule };
|
||||
fetch(url, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}).then(function () { isScreen ? screenTriggerLoadAll() : triggerLoadAll(); });
|
||||
}).then(() => { isScreen ? screenTriggerLoadAll() : triggerLoadAll(); });
|
||||
}
|
||||
|
||||
// Construit une FEUILLE du type demandé — passe par ses éventuelles
|
||||
@@ -188,53 +194,56 @@ function collisionWizardBuildLeafAction(type, onBuilt) {
|
||||
return;
|
||||
}
|
||||
if (type === 'surbrillance' || type === 'visibilite') {
|
||||
renderCollisionWizardObjectTargetStep(function (objectId) {
|
||||
renderCollisionWizardValeurStep(type, function (valeur) {
|
||||
onBuilt({ type: type, id: collisionGenerateActionId(), object_id: objectId, valeur: valeur });
|
||||
renderCollisionWizardObjectTargetStep((objectId) => {
|
||||
renderCollisionWizardValeurStep(type, (valeur) => {
|
||||
onBuilt({
|
||||
type, id: collisionGenerateActionId(), object_id: objectId, valeur,
|
||||
});
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (type === 'son') {
|
||||
renderCollisionWizardAssetStep(function (assetUrl) {
|
||||
renderCollisionWizardAssetStep((assetUrl) => {
|
||||
onBuilt({ type: 'son', id: collisionGenerateActionId(), asset_url: assetUrl });
|
||||
}, 'audio');
|
||||
return;
|
||||
}
|
||||
if (type === 'video') {
|
||||
renderCollisionWizardAssetStep(function (assetUrl) {
|
||||
renderCollisionWizardVideoModeStep(function (mode) {
|
||||
onBuilt({ type: 'video', id: collisionGenerateActionId(), asset_url: assetUrl, mode: mode });
|
||||
renderCollisionWizardAssetStep((assetUrl) => {
|
||||
renderCollisionWizardVideoModeStep((mode) => {
|
||||
onBuilt({
|
||||
type: 'video', id: collisionGenerateActionId(), asset_url: assetUrl, mode,
|
||||
});
|
||||
});
|
||||
}, 'video');
|
||||
return;
|
||||
}
|
||||
if (type === 'indication') {
|
||||
renderCollisionWizardObjectTargetStep(function (objectId) {
|
||||
renderCollisionWizardObjectTargetStep((objectId) => {
|
||||
_collisionWizard.pendingIndicationObjectId = objectId;
|
||||
renderCollisionWizardIndicationStep(function (action) { onBuilt(action); });
|
||||
renderCollisionWizardIndicationStep((action) => { onBuilt(action); });
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 'attendre'
|
||||
renderCollisionWizardAttendreStep(function (secondes) {
|
||||
renderCollisionWizardAttendreStep((secondes) => {
|
||||
onBuilt({ type: 'attendre', id: collisionGenerateActionId(), data_value: secondes });
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- Étape "attendre" (nombre de secondes) ----------
|
||||
function renderCollisionWizardAttendreStep(onChoose) {
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Attendre combien de secondes avant de continuer ?</p>' +
|
||||
'<input type="number" id="attendreWizSecondes" min="0" step="0.5" placeholder="2" style="width:100px; margin-bottom:10px;">' +
|
||||
'<button type="button" class="button primary" onclick="collisionWizardAttendreNext()">Continuer</button>';
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Attendre combien de secondes avant de continuer ?</p>'
|
||||
+ '<input type="number" id="attendreWizSecondes" min="0" step="0.5" placeholder="2" style="width:100px; margin-bottom:10px;">'
|
||||
+ '<button type="button" class="button primary" onclick="collisionWizardAttendreNext()">Continuer</button>';
|
||||
_collisionWizard.onAttendreChosen = onChoose;
|
||||
}
|
||||
|
||||
function collisionWizardAttendreNext() {
|
||||
var raw = document.getElementById('attendreWizSecondes').value;
|
||||
var secondes = String(Math.max(0, parseFloat(raw) || 0));
|
||||
var onChoose = _collisionWizard.onAttendreChosen;
|
||||
const raw = document.getElementById('attendreWizSecondes').value;
|
||||
const secondes = String(Math.max(0, parseFloat(raw) || 0));
|
||||
const onChoose = _collisionWizard.onAttendreChosen;
|
||||
_collisionWizard.onAttendreChosen = null;
|
||||
onChoose(secondes);
|
||||
}
|
||||
@@ -246,27 +255,24 @@ function collisionWizardAttendreNext() {
|
||||
// forcément celui qui porte la règle (ex. le joueur touche un
|
||||
// interrupteur -> une indication apparaît près d'une porte ailleurs).
|
||||
function renderCollisionWizardObjectTargetStep(onChoose) {
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Sur quel objet ?</p>' +
|
||||
'<div id="collisionWizardObjectTargetGrid" class="collisionWizardChoices">Chargement…</div>';
|
||||
fetch('/game/' + GAME_SLUG + '/triggers/objects-catalog')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (objects) {
|
||||
var grid = document.getElementById('collisionWizardObjectTargetGrid');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Sur quel objet ?</p>'
|
||||
+ '<div id="collisionWizardObjectTargetGrid" class="collisionWizardChoices">Chargement…</div>';
|
||||
fetch(`/game/${GAME_SLUG}/triggers/objects-catalog`)
|
||||
.then((r) => r.json())
|
||||
.then((objects) => {
|
||||
const grid = document.getElementById('collisionWizardObjectTargetGrid');
|
||||
if (!grid) return;
|
||||
if (!objects.length) { grid.innerHTML = '<p class="hint">Aucun objet éligible sur ce jeu.</p>'; return; }
|
||||
grid.innerHTML = objects.map(function (o, i) {
|
||||
return '<button type="button" class="collisionWizardChoice questTriggerObjectChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardObjectTargetChosen(' + o.id + ')">' +
|
||||
'<div class="questTriggerObjectThumb">' + collisionRuleThumbHtml(o) + '</div>' +
|
||||
'<span>' + (o.name || '').replace(/</g, '<') + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
grid.innerHTML = objects.map((o, i) => `<button type="button" class="collisionWizardChoice questTriggerObjectChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardObjectTargetChosen(${o.id})">`
|
||||
+ `<div class="questTriggerObjectThumb">${collisionRuleThumbHtml(o)}</div>`
|
||||
+ `<span>${(o.name || '').replace(/</g, '<')}</span>`
|
||||
+ '</button>').join('');
|
||||
_collisionWizard.onObjectTargetChosen = onChoose;
|
||||
});
|
||||
}
|
||||
|
||||
function collisionWizardObjectTargetChosen(objectId) {
|
||||
var onChoose = _collisionWizard.onObjectTargetChosen;
|
||||
const onChoose = _collisionWizard.onObjectTargetChosen;
|
||||
_collisionWizard.onObjectTargetChosen = null;
|
||||
onChoose(objectId);
|
||||
}
|
||||
@@ -276,21 +282,18 @@ function collisionWizardObjectTargetChosen(objectId) {
|
||||
// templates/scene_edit.html (voir screens/labels/surbrillance_values.py
|
||||
// et screens/labels/element_visibility.py — mêmes valeurs que le moteur).
|
||||
function renderCollisionWizardValeurStep(actionType, onChoose) {
|
||||
var labelsMap = actionType === 'surbrillance' ? SURBRILLANCE_LABELS_MAP : ELEMENT_VISIBILITY_LABELS_MAP;
|
||||
var keys = Object.keys(labelsMap);
|
||||
var buttons = keys.map(function (key, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardValeurChosen(\'' + key + '\')">' +
|
||||
'<span>' + labelsMap[key] + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">' + (actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :') + '</p>' +
|
||||
'<div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
const labelsMap = actionType === 'surbrillance' ? SURBRILLANCE_LABELS_MAP : ELEMENT_VISIBILITY_LABELS_MAP;
|
||||
const keys = Object.keys(labelsMap);
|
||||
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardValeurChosen('${key}')">`
|
||||
+ `<span>${labelsMap[key]}</span>`
|
||||
+ '</button>').join('');
|
||||
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :'}</p>`
|
||||
+ `<div class="collisionWizardChoices">${buttons}</div>`;
|
||||
_collisionWizard.onValeurChosen = onChoose;
|
||||
}
|
||||
|
||||
function collisionWizardValeurChosen(valeur) {
|
||||
var onChoose = _collisionWizard.onValeurChosen;
|
||||
const onChoose = _collisionWizard.onValeurChosen;
|
||||
_collisionWizard.onValeurChosen = null;
|
||||
onChoose(valeur);
|
||||
}
|
||||
@@ -305,46 +308,39 @@ function collisionWizardValeurChosen(valeur) {
|
||||
// inversement pour "son"), un fichier du mauvais type ne pouvant de toute
|
||||
// façon jamais être lu par <video>/<audio> côté jeu.
|
||||
function renderCollisionWizardAssetStep(onChoose, kind) {
|
||||
var assets = USER_ASSETS_OPTIONS.filter(function (a) { return a.kind === kind; });
|
||||
const assets = USER_ASSETS_OPTIONS.filter((a) => a.kind === kind);
|
||||
if (!assets.length) {
|
||||
var label = kind === 'video' ? 'vidéo (mp4/webm)' : 'son';
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Aucun fichier ' + label + ' dans "Mes assets" pour l\'instant — importe-en un depuis le panneau de gauche (section "Mes assets"), puis recommence.</p>';
|
||||
const label = kind === 'video' ? 'vidéo (mp4/webm)' : 'son';
|
||||
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">Aucun fichier ${label} dans "Mes assets" pour l'instant — importe-en un depuis le panneau de gauche (section "Mes assets"), puis recommence.</p>`;
|
||||
return;
|
||||
}
|
||||
var options = assets.map(function (a) {
|
||||
return '<option value="' + a.url + '">' + (a.name || '').replace(/</g, '<') + '</option>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Quel fichier (voir "Mes assets") ?</p>' +
|
||||
'<select id="collisionWizardAssetSelect" style="width:100%; margin-bottom:10px;">' + options + '</select>' +
|
||||
'<button type="button" class="button primary" onclick="collisionWizardAssetNext()">Continuer</button>';
|
||||
const options = assets.map((a) => `<option value="${a.url}">${(a.name || '').replace(/</g, '<')}</option>`).join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Quel fichier (voir "Mes assets") ?</p>'
|
||||
+ `<select id="collisionWizardAssetSelect" style="width:100%; margin-bottom:10px;">${options}</select>`
|
||||
+ '<button type="button" class="button primary" onclick="collisionWizardAssetNext()">Continuer</button>';
|
||||
_collisionWizard.onAssetChosen = onChoose;
|
||||
}
|
||||
|
||||
function collisionWizardAssetNext() {
|
||||
var url = document.getElementById('collisionWizardAssetSelect').value;
|
||||
var onChoose = _collisionWizard.onAssetChosen;
|
||||
const url = document.getElementById('collisionWizardAssetSelect').value;
|
||||
const onChoose = _collisionWizard.onAssetChosen;
|
||||
_collisionWizard.onAssetChosen = null;
|
||||
onChoose(url);
|
||||
}
|
||||
|
||||
// ---------- Étape "comment l'afficher ?" (vidéo) ----------
|
||||
function renderCollisionWizardVideoModeStep(onChoose) {
|
||||
var keys = Object.keys(VIDEO_MODE_LABELS_MAP);
|
||||
var buttons = keys.map(function (key, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardVideoModeChosen(\'' + key + '\')">' +
|
||||
'<span>' + VIDEO_MODE_LABELS_MAP[key] + '</span>' +
|
||||
'</button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Comment l\'afficher ?</p>' +
|
||||
'<div class="collisionWizardChoices">' + buttons + '</div>';
|
||||
const keys = Object.keys(VIDEO_MODE_LABELS_MAP);
|
||||
const buttons = keys.map((key, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardVideoModeChosen('${key}')">`
|
||||
+ `<span>${VIDEO_MODE_LABELS_MAP[key]}</span>`
|
||||
+ '</button>').join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Comment l\'afficher ?</p>'
|
||||
+ `<div class="collisionWizardChoices">${buttons}</div>`;
|
||||
_collisionWizard.onVideoModeChosen = onChoose;
|
||||
}
|
||||
|
||||
function collisionWizardVideoModeChosen(mode) {
|
||||
var onChoose = _collisionWizard.onVideoModeChosen;
|
||||
const onChoose = _collisionWizard.onVideoModeChosen;
|
||||
_collisionWizard.onVideoModeChosen = null;
|
||||
onChoose(mode);
|
||||
}
|
||||
@@ -354,22 +350,23 @@ function collisionWizardVideoModeChosen(mode) {
|
||||
// collisionWizardBuildLeafAction) — mémorisé sur le wizard le temps de
|
||||
// cette étape plutôt que reporté en paramètre partout.
|
||||
function renderCollisionWizardIndicationStep(onChoose) {
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Texte de la bulle :</p>' +
|
||||
'<input type="text" id="indicationWizText" placeholder="Ex. Clique ici pour continuer" style="width:100%; margin-bottom:8px;">' +
|
||||
'<label class="hint" style="display:block; margin-bottom:10px;">Durée d\'affichage en ms (optionnel, 3000 par défaut) : ' +
|
||||
'<input type="number" id="indicationWizDuree" min="500" step="500" placeholder="3000" style="width:100px;"></label>' +
|
||||
'<button type="button" class="button primary" onclick="collisionWizardIndicationNext()">Continuer</button>';
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Texte de la bulle :</p>'
|
||||
+ '<input type="text" id="indicationWizText" placeholder="Ex. Clique ici pour continuer" style="width:100%; margin-bottom:8px;">'
|
||||
+ '<label class="hint" style="display:block; margin-bottom:10px;">Durée d\'affichage en ms (optionnel, 3000 par défaut) : '
|
||||
+ '<input type="number" id="indicationWizDuree" min="500" step="500" placeholder="3000" style="width:100px;"></label>'
|
||||
+ '<button type="button" class="button primary" onclick="collisionWizardIndicationNext()">Continuer</button>';
|
||||
_collisionWizard.onIndicationChosen = onChoose;
|
||||
}
|
||||
|
||||
function collisionWizardIndicationNext() {
|
||||
var texte = document.getElementById('indicationWizText').value;
|
||||
var dureeRaw = document.getElementById('indicationWizDuree').value;
|
||||
var action = { type: 'indication', id: collisionGenerateActionId(), object_id: _collisionWizard.pendingIndicationObjectId, texte: texte };
|
||||
const texte = document.getElementById('indicationWizText').value;
|
||||
const dureeRaw = document.getElementById('indicationWizDuree').value;
|
||||
const action = {
|
||||
type: 'indication', id: collisionGenerateActionId(), object_id: _collisionWizard.pendingIndicationObjectId, texte,
|
||||
};
|
||||
if (dureeRaw) action.duree_ms = parseInt(dureeRaw, 10);
|
||||
_collisionWizard.pendingIndicationObjectId = null;
|
||||
var onChoose = _collisionWizard.onIndicationChosen;
|
||||
const onChoose = _collisionWizard.onIndicationChosen;
|
||||
_collisionWizard.onIndicationChosen = null;
|
||||
onChoose(action);
|
||||
}
|
||||
@@ -384,15 +381,15 @@ function collisionWizardIndicationNext() {
|
||||
// mini-formulaire (voir renderCollisionWizardConditionStep).
|
||||
function collisionWizardChooseAction(type, nested) {
|
||||
if (type === 'interagir') {
|
||||
collisionWizardFinalizeAction({ type: type }, nested);
|
||||
collisionWizardFinalizeAction({ type }, nested);
|
||||
return;
|
||||
}
|
||||
if (type === 'condition') {
|
||||
renderCollisionWizardConditionStep(nested);
|
||||
return;
|
||||
}
|
||||
collisionWizardBuildLeafAction(type, function (leaf) {
|
||||
renderCollisionWizardChainStep(leaf, function (finalLeaf) { collisionWizardFinalizeAction(finalLeaf, nested); });
|
||||
collisionWizardBuildLeafAction(type, (leaf) => {
|
||||
renderCollisionWizardChainStep(leaf, (finalLeaf) => { collisionWizardFinalizeAction(finalLeaf, nested); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -404,40 +401,38 @@ function collisionWizardChooseAction(type, nested) {
|
||||
// valeur (masquée pour les opérations qui n'en ont pas besoin, voir
|
||||
// COLLISION_NO_VALUE_OPERATIONS). `onChoose(action)`.
|
||||
function renderCollisionWizardVariableStep(onChoose) {
|
||||
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
|
||||
var operationOptions = Object.keys(DATA_OPERATION_LABELS_MAP).map(function (key) {
|
||||
return '<option value="' + key + '">' + DATA_OPERATION_LABELS_MAP[key] + '</option>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Modifier...</p>' +
|
||||
'<div class="collisionWizardConditionForm">' +
|
||||
'<select id="varWizVariable">' + variableOptions + '</select>' +
|
||||
'<select id="varWizOperation" onchange="collisionWizardVariableOperationChanged()">' + operationOptions + '</select>' +
|
||||
'<input type="text" id="varWizValue" placeholder="Valeur">' +
|
||||
'</div>' +
|
||||
'<button type="button" class="button primary mt-3" onclick="collisionWizardVariableNext()">Continuer</button>';
|
||||
const variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map((name) => `<option value="${name}">${name}</option>`).join('');
|
||||
const operationOptions = Object.keys(DATA_OPERATION_LABELS_MAP).map((key) => `<option value="${key}">${DATA_OPERATION_LABELS_MAP[key]}</option>`).join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Modifier...</p>'
|
||||
+ '<div class="collisionWizardConditionForm">'
|
||||
+ `<select id="varWizVariable">${variableOptions}</select>`
|
||||
+ `<select id="varWizOperation" onchange="collisionWizardVariableOperationChanged()">${operationOptions}</select>`
|
||||
+ '<input type="text" id="varWizValue" placeholder="Valeur">'
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="button primary mt-3" onclick="collisionWizardVariableNext()">Continuer</button>';
|
||||
_collisionWizard.onVariableChosen = onChoose;
|
||||
collisionWizardVariableOperationChanged();
|
||||
}
|
||||
|
||||
function collisionWizardVariableOperationChanged() {
|
||||
var operation = document.getElementById('varWizOperation').value;
|
||||
var valueInput = document.getElementById('varWizValue');
|
||||
const operation = document.getElementById('varWizOperation').value;
|
||||
const valueInput = document.getElementById('varWizValue');
|
||||
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1 ? '' : 'none';
|
||||
}
|
||||
|
||||
function collisionWizardVariableNext() {
|
||||
var operation = document.getElementById('varWizOperation').value;
|
||||
var action = {
|
||||
type: 'variable', id: collisionGenerateActionId(),
|
||||
const operation = document.getElementById('varWizOperation').value;
|
||||
const action = {
|
||||
type: 'variable',
|
||||
id: collisionGenerateActionId(),
|
||||
target_variable: document.getElementById('varWizVariable').value,
|
||||
data_operation: operation,
|
||||
};
|
||||
if (COLLISION_NO_VALUE_OPERATIONS.indexOf(operation) === -1) {
|
||||
action.data_value = document.getElementById('varWizValue').value;
|
||||
}
|
||||
var onChoose = _collisionWizard.onVariableChosen;
|
||||
const onChoose = _collisionWizard.onVariableChosen;
|
||||
_collisionWizard.onVariableChosen = null;
|
||||
onChoose(action);
|
||||
}
|
||||
@@ -461,30 +456,27 @@ function collisionWizardVariableNext() {
|
||||
// dialogue, "attendre" disparaissait silencieusement.
|
||||
function renderCollisionWizardChainStep(leafAction, onDone, headAction) {
|
||||
headAction = headAction || leafAction;
|
||||
var completionHtml = leafAction.type === 'dialogue'
|
||||
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">' +
|
||||
'<input type="checkbox" id="condWizMarkCompleted"' + (leafAction.mark_completed ? ' checked' : '') + '> Marquer ce dialogue comme terminé une fois joué' +
|
||||
'</label>'
|
||||
const completionHtml = leafAction.type === 'dialogue'
|
||||
? '<label style="display:flex; align-items:center; gap:6px; font-size:13px; margin-bottom:10px;">'
|
||||
+ `<input type="checkbox" id="condWizMarkCompleted"${leafAction.mark_completed ? ' checked' : ''}> Marquer ce dialogue comme terminé une fois joué`
|
||||
+ '</label>'
|
||||
: '';
|
||||
var choiceButtons = COLLISION_CHAIN_CHOICES.map(function (choice, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + ((i + 1) * 40) + 'ms" onclick="collisionWizardChainDone(\'' + choice + '\')"><span>' + COLLISION_ACTION_LABELS[choice] + '</span></button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
completionHtml +
|
||||
'<p class="hint mb-2">Ensuite ?</p>' +
|
||||
'<div class="collisionWizardChoices">' +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChainDone(\'none\')"><span>Rien de plus</span></button>' +
|
||||
choiceButtons +
|
||||
'</div>';
|
||||
const choiceButtons = COLLISION_CHAIN_CHOICES.map((choice, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${(i + 1) * 40}ms" onclick="collisionWizardChainDone('${choice}')"><span>${COLLISION_ACTION_LABELS[choice]}</span></button>`).join('');
|
||||
collisionWizardBodyEl().innerHTML = `${completionHtml
|
||||
}<p class="hint mb-2">Ensuite ?</p>`
|
||||
+ '<div class="collisionWizardChoices">'
|
||||
+ `<button type="button" class="collisionWizardChoice" style="animation-delay:0ms" onclick="collisionWizardChainDone('none')"><span>Rien de plus</span></button>${
|
||||
choiceButtons
|
||||
}</div>`;
|
||||
_collisionWizard.chainLeaf = leafAction;
|
||||
_collisionWizard.chainHead = headAction;
|
||||
_collisionWizard.onChainDone = onDone;
|
||||
}
|
||||
|
||||
function collisionWizardChainDone(choice) {
|
||||
var leafAction = _collisionWizard.chainLeaf;
|
||||
var headAction = _collisionWizard.chainHead;
|
||||
var onDone = _collisionWizard.onChainDone;
|
||||
const leafAction = _collisionWizard.chainLeaf;
|
||||
const headAction = _collisionWizard.chainHead;
|
||||
const onDone = _collisionWizard.onChainDone;
|
||||
if (leafAction.type === 'dialogue') {
|
||||
if (document.getElementById('condWizMarkCompleted').checked) leafAction.mark_completed = true;
|
||||
else delete leafAction.mark_completed;
|
||||
@@ -493,8 +485,8 @@ function collisionWizardChainDone(choice) {
|
||||
onDone(headAction);
|
||||
return;
|
||||
}
|
||||
collisionWizardBuildLeafAction(choice, function (nextLeaf) {
|
||||
leafAction.then = nextLeaf;
|
||||
collisionWizardBuildLeafAction(choice, (nextLeaf) => {
|
||||
leafAction.then = nextLeaf; // NOSONAR S7739 - chainage d'actions (donnee), jamais une promesse/await
|
||||
renderCollisionWizardChainStep(nextLeaf, onDone, headAction);
|
||||
});
|
||||
}
|
||||
@@ -506,19 +498,16 @@ function collisionWizardChainDone(choice) {
|
||||
// `nested` (interagir -> condition) est reporté jusqu'à la finalisation,
|
||||
// une fois les deux branches Vrai/Faux choisies ci-dessous.
|
||||
function renderCollisionWizardConditionStep(nested) {
|
||||
var variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map(function (name) { return '<option value="' + name + '">' + name + '</option>'; }).join('');
|
||||
var operatorOptions = Object.keys(CONDITION_OPERATOR_LABELS_MAP).map(function (key) {
|
||||
return '<option value="' + key + '">' + CONDITION_OPERATOR_LABELS_MAP[key] + '</option>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">Si...</p>' +
|
||||
'<div class="collisionWizardConditionForm">' +
|
||||
'<select id="condWizVariable">' + variableOptions + '</select>' +
|
||||
'<select id="condWizOperator">' + operatorOptions + '</select>' +
|
||||
'<input type="text" id="condWizValue" placeholder="Valeur">' +
|
||||
'</div>' +
|
||||
'<button type="button" class="button primary mt-3" onclick="collisionWizardConditionNext(' + (nested ? 'true' : 'false') + ')">Continuer</button>';
|
||||
const variableOptions = (typeof GLOBAL_VARIABLE_NAMES !== 'undefined' ? GLOBAL_VARIABLE_NAMES : [])
|
||||
.map((name) => `<option value="${name}">${name}</option>`).join('');
|
||||
const operatorOptions = Object.keys(CONDITION_OPERATOR_LABELS_MAP).map((key) => `<option value="${key}">${CONDITION_OPERATOR_LABELS_MAP[key]}</option>`).join('');
|
||||
collisionWizardBodyEl().innerHTML = '<p class="hint mb-2">Si...</p>'
|
||||
+ '<div class="collisionWizardConditionForm">'
|
||||
+ `<select id="condWizVariable">${variableOptions}</select>`
|
||||
+ `<select id="condWizOperator">${operatorOptions}</select>`
|
||||
+ '<input type="text" id="condWizValue" placeholder="Valeur">'
|
||||
+ '</div>'
|
||||
+ `<button type="button" class="button primary mt-3" onclick="collisionWizardConditionNext(${nested ? 'true' : 'false'})">Continuer</button>`;
|
||||
}
|
||||
|
||||
function collisionWizardConditionNext(nested) {
|
||||
@@ -527,7 +516,8 @@ function collisionWizardConditionNext(nested) {
|
||||
variable: document.getElementById('condWizVariable').value,
|
||||
operateur: document.getElementById('condWizOperator').value,
|
||||
valeur: document.getElementById('condWizValue').value,
|
||||
si_vrai: null, si_faux: null,
|
||||
si_vrai: null,
|
||||
si_faux: null,
|
||||
};
|
||||
_collisionWizard.pendingConditionNested = nested;
|
||||
renderCollisionWizardBranchStep('si_vrai');
|
||||
@@ -541,18 +531,15 @@ function collisionWizardConditionNext(nested) {
|
||||
// peuvent chacune déclencher LEUR PROPRE feuille (et sa propre chaîne),
|
||||
// indépendamment l'une de l'autre.
|
||||
function renderCollisionWizardBranchStep(branchKey) {
|
||||
var label = branchKey === 'si_vrai' ? '✓ Si VRAI...' : '✕ Si FAUX...';
|
||||
var choiceButtons = COLLISION_CHAIN_CHOICES.map(function (choice, i) {
|
||||
return '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'' + choice + '\')"><span>' + COLLISION_ACTION_LABELS[choice] + '</span></button>';
|
||||
}).join('');
|
||||
collisionWizardBodyEl().innerHTML =
|
||||
'<p class="hint mb-2">' + label + '</p>' +
|
||||
'<div class="collisionWizardChoices">' +
|
||||
choiceButtons +
|
||||
'<button type="button" class="collisionWizardChoice" style="animation-delay:' + (COLLISION_CHAIN_CHOICES.length * 40) + 'ms" onclick="collisionWizardChooseBranch(\'' + branchKey + '\', \'none\')">' +
|
||||
'<span>Ne rien faire</span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
const label = branchKey === 'si_vrai' ? '✓ Si VRAI...' : '✕ Si FAUX...';
|
||||
const choiceButtons = COLLISION_CHAIN_CHOICES.map((choice, i) => `<button type="button" class="collisionWizardChoice" style="animation-delay:${i * 40}ms" onclick="collisionWizardChooseBranch('${branchKey}', '${choice}')"><span>${COLLISION_ACTION_LABELS[choice]}</span></button>`).join('');
|
||||
collisionWizardBodyEl().innerHTML = `<p class="hint mb-2">${label}</p>`
|
||||
+ `<div class="collisionWizardChoices">${
|
||||
choiceButtons
|
||||
}<button type="button" class="collisionWizardChoice" style="animation-delay:${COLLISION_CHAIN_CHOICES.length * 40}ms" onclick="collisionWizardChooseBranch('${branchKey}', 'none')">`
|
||||
+ '<span>Ne rien faire</span>'
|
||||
+ '</button>'
|
||||
+ '</div>';
|
||||
}
|
||||
|
||||
function collisionWizardChooseBranch(branchKey, choice) {
|
||||
@@ -561,8 +548,8 @@ function collisionWizardChooseBranch(branchKey, choice) {
|
||||
collisionWizardBranchDone(branchKey);
|
||||
return;
|
||||
}
|
||||
collisionWizardBuildLeafAction(choice, function (leaf) {
|
||||
renderCollisionWizardChainStep(leaf, function (finalLeaf) {
|
||||
collisionWizardBuildLeafAction(choice, (leaf) => {
|
||||
renderCollisionWizardChainStep(leaf, (finalLeaf) => {
|
||||
_collisionWizard.pendingCondition[branchKey] = finalLeaf;
|
||||
collisionWizardBranchDone(branchKey);
|
||||
});
|
||||
@@ -574,8 +561,8 @@ function collisionWizardBranchDone(branchKey) {
|
||||
renderCollisionWizardBranchStep('si_faux');
|
||||
return;
|
||||
}
|
||||
var nested = _collisionWizard.pendingConditionNested;
|
||||
var condition = _collisionWizard.pendingCondition;
|
||||
const nested = _collisionWizard.pendingConditionNested;
|
||||
const condition = _collisionWizard.pendingCondition;
|
||||
_collisionWizard.pendingCondition = null;
|
||||
collisionWizardFinalizeAction(condition, nested);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user