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>
570 lines
29 KiB
JavaScript
570 lines
29 KiB
JavaScript
// ---------- Assistant "déclencheur -> action" (fusionné dans l'onglet "Déclencheurs") ----------
|
|
// L'onglet "🧩 Collision" séparé a été retiré (redondant une fois la
|
|
// vue/gestion des déclencheurs intégrée à l'onglet "Déclencheurs", voir
|
|
// static/js/triggers/trigger-editor.js::openAddTriggerModal) — ce
|
|
// fichier ne porte plus que le MOTEUR de l'assistant lui-même
|
|
// (déclencheur -> action, un clic choisit une option, la carte suivante
|
|
// s'emboîte aussitôt), invoqué exclusivement via openTriggerWizard()
|
|
// ci-dessous. Voir screens/rendering/collision_rules.py pour la forme
|
|
// exacte d'une règle (côté serveur, où elle est validée une seconde fois
|
|
// avant d'être enregistrée).
|
|
|
|
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',
|
|
};
|
|
// 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.
|
|
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.
|
|
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.
|
|
const COLLISION_NO_VALUE_OPERATIONS = new Set(['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
|
|
// screens/scenes/render_scene_object.py) — un personnage déplacé loin
|
|
// dans la scène (left/top de plusieurs centaines de px) ressortait donc
|
|
// hors du petit cadre de la miniature : la règle CSS `.questTriggerObjectCard
|
|
// img`/`.questTriggerObjectThumb img` (static/style.css) est censée le
|
|
// neutraliser, mais s'appuyer sur un forçage CSS pour annuler un style
|
|
// inline entier reste fragile. Plus robuste : reconstruire ici une balise
|
|
// <img> "propre", sans AUCUN style hérité du canevas — seul le src
|
|
// (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) {
|
|
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
|
|
// "variable", voir screens/rendering/collision_rules.py) — jamais
|
|
// recalculé/réattribué côté serveur. Pour un dialogue, sert de
|
|
// dialogue.id (édité en ligne, voir trigger-editor.js) ; pour une
|
|
// variable, sert d'id à la feuille elle-même — dans les deux cas,
|
|
// 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)}`; // NOSONAR S2245 - id local UI, jamais crypto
|
|
}
|
|
|
|
// ---------- Assistant "déclencheur -> action" ----------
|
|
// État du parcours en cours — un seul à la fois (la modale n'autorise
|
|
// qu'une règle en construction) : l'objet ciblé, le déclencheur choisi,
|
|
// et l'action en cours de construction (posée directement dans
|
|
// `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.
|
|
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, trigger: null, bodyEl,
|
|
};
|
|
renderCollisionWizardTriggerStep();
|
|
}
|
|
|
|
// Section "Déclencheurs de l'écran" (voir static/js/triggers/
|
|
// trigger-editor.js::openScreenAddTriggerModal) — même assistant, mais
|
|
// SANS objet propriétaire (voir screens/rendering/screen_triggers.py) :
|
|
// 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, trigger: null, bodyEl,
|
|
};
|
|
renderCollisionWizardTriggerStep();
|
|
}
|
|
|
|
function collisionWizardBodyEl() {
|
|
return _collisionWizard.bodyEl;
|
|
}
|
|
|
|
// Étape 1 — déclencheur : "collision"/"clic"/"survol" pour un objet,
|
|
// "affichage" (seul choix) pour un déclencheur d'écran — la carte à choix
|
|
// 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() {
|
|
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) {
|
|
_collisionWizard.trigger = trigger;
|
|
renderCollisionWizardActionStep(false);
|
|
}
|
|
|
|
// Étape 2 (et, pour "interagir", étape 3 en second niveau — `nested`
|
|
// distingue les deux, "interagir" n'étant proposé qu'au premier niveau,
|
|
// voir screens/rendering/collision_rules.py::_MAX_ACTION_DEPTH) :
|
|
// quelle action exécuter. "interagir" n'a de sens QUE pour "collision"
|
|
// (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) {
|
|
const types = (nested || _collisionWizard.trigger !== 'collision')
|
|
? COLLISION_WIZARD_ACTION_TYPES.filter((t) => t !== 'interagir')
|
|
: COLLISION_WIZARD_ACTION_TYPES;
|
|
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) {
|
|
if (nested) {
|
|
// Deuxième niveau ("interagir" -> sous-action) : referme la maille
|
|
// sur l'action de premier niveau, déjà mémorisée sur le wizard.
|
|
_collisionWizard.pendingInteragir.sub_action = action;
|
|
collisionWizardSaveRule(_collisionWizard.pendingInteragir);
|
|
return;
|
|
}
|
|
if (action.type === 'interagir') {
|
|
// "interagir" n'est jamais final : une carte supplémentaire s'emboîte
|
|
// pour choisir CE QUI se passe une fois la touche pressée.
|
|
_collisionWizard.pendingInteragir = action;
|
|
renderCollisionWizardActionStep(true);
|
|
return;
|
|
}
|
|
collisionWizardSaveRule(action);
|
|
}
|
|
|
|
// Enregistre via /game/<slug>/triggers (objet, routes/triggers/triggers.py)
|
|
// ou /game/<slug>/screens/<id>/triggers (écran, routes/screens/
|
|
// screen_triggers.py) selon _collisionWizard.target — même contrat
|
|
// "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) {
|
|
const rule = { trigger: _collisionWizard.trigger, action };
|
|
closeTriggerModal();
|
|
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' },
|
|
body: JSON.stringify(body),
|
|
}).then(() => { isScreen ? screenTriggerLoadAll() : triggerLoadAll(); });
|
|
}
|
|
|
|
// Construit une FEUILLE du type demandé — passe par ses éventuelles
|
|
// étapes intermédiaires (objet ciblé, valeur, fichier "Mes assets"...)
|
|
// puis appelle `onBuilt(action)`. PARTAGÉ entre le premier choix
|
|
// d'action (collisionWizardChooseAction), le choix "Ensuite ?"
|
|
// (collisionWizardChainDone), les branches Vrai/Faux d'une condition
|
|
// (collisionWizardChooseBranch) et "+ Ajouter une action" sur un
|
|
// déclencheur déjà posé (trigger-editor.js::triggerAppendLeaf/
|
|
// screenTriggerAppendLeaf) — ces quatre points d'entrée ont besoin
|
|
// EXACTEMENT du même parcours pour chaque type de feuille.
|
|
function collisionWizardBuildLeafAction(type, onBuilt) {
|
|
if (type === 'dialogue') {
|
|
onBuilt({ type: 'dialogue', dialogue: { id: collisionGenerateActionId(), lines: [] } });
|
|
return;
|
|
}
|
|
if (type === 'variable') {
|
|
renderCollisionWizardVariableStep(onBuilt);
|
|
return;
|
|
}
|
|
if (type === 'surbrillance' || type === 'visibilite') {
|
|
renderCollisionWizardObjectTargetStep((objectId) => {
|
|
renderCollisionWizardValeurStep(type, (valeur) => {
|
|
onBuilt({
|
|
type, id: collisionGenerateActionId(), object_id: objectId, valeur,
|
|
});
|
|
});
|
|
});
|
|
return;
|
|
}
|
|
if (type === 'son') {
|
|
renderCollisionWizardAssetStep((assetUrl) => {
|
|
onBuilt({ type: 'son', id: collisionGenerateActionId(), asset_url: assetUrl });
|
|
}, 'audio');
|
|
return;
|
|
}
|
|
if (type === 'video') {
|
|
renderCollisionWizardAssetStep((assetUrl) => {
|
|
renderCollisionWizardVideoModeStep((mode) => {
|
|
onBuilt({
|
|
type: 'video', id: collisionGenerateActionId(), asset_url: assetUrl, mode,
|
|
});
|
|
});
|
|
}, 'video');
|
|
return;
|
|
}
|
|
if (type === 'indication') {
|
|
renderCollisionWizardObjectTargetStep((objectId) => {
|
|
_collisionWizard.pendingIndicationObjectId = objectId;
|
|
renderCollisionWizardIndicationStep((action) => { onBuilt(action); });
|
|
});
|
|
return;
|
|
}
|
|
// 'attendre'
|
|
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>';
|
|
_collisionWizard.onAttendreChosen = onChoose;
|
|
}
|
|
|
|
function collisionWizardAttendreNext() {
|
|
const raw = document.getElementById('attendreWizSecondes').value;
|
|
const secondes = String(Math.max(0, Number.parseFloat(raw) || 0));
|
|
const onChoose = _collisionWizard.onAttendreChosen;
|
|
_collisionWizard.onAttendreChosen = null;
|
|
onChoose(secondes);
|
|
}
|
|
|
|
// ---------- Étape "Sur quel objet ?" (surbrillance/visibilite/indication) ----------
|
|
// Réutilise le MÊME catalogue que le choix d'objet propriétaire du
|
|
// déclencheur (voir triggerRenderObjectStep, trigger-editor.js) : ces
|
|
// trois actions peuvent cibler N'IMPORTE QUEL objet éligible du jeu, pas
|
|
// 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((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((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 || '').replaceAll('<', '<')}</span>`
|
|
+ '</button>').join('');
|
|
_collisionWizard.onObjectTargetChosen = onChoose;
|
|
});
|
|
}
|
|
|
|
function collisionWizardObjectTargetChosen(objectId) {
|
|
const onChoose = _collisionWizard.onObjectTargetChosen;
|
|
_collisionWizard.onObjectTargetChosen = null;
|
|
onChoose(objectId);
|
|
}
|
|
|
|
// ---------- Étape "valeur" (surbrillance/visibilite) ----------
|
|
// SURBRILLANCE_LABELS_MAP/ELEMENT_VISIBILITY_LABELS_MAP posés par
|
|
// 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) {
|
|
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) {
|
|
const onChoose = _collisionWizard.onValeurChosen;
|
|
_collisionWizard.onValeurChosen = null;
|
|
onChoose(valeur);
|
|
}
|
|
|
|
// ---------- Étape "quel fichier ?" (son/vidéo) ----------
|
|
// USER_ASSETS_OPTIONS posé par templates/scene_edit.html ([{id, url,
|
|
// name, kind}], voir auth/user_asset_kind.py) — le créateur doit d'abord
|
|
// importer un fichier dans "Mes assets" (panneau de gauche) avant de
|
|
// pouvoir le choisir ici. `kind` ("audio"/"video", demande explicite : "la
|
|
// vidéo devra être en mp4 ou webm") filtre la liste au bon type — sans
|
|
// ça, l'action "vidéo" proposait AUSSI les images et les sons (et
|
|
// 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) {
|
|
const assets = USER_ASSETS_OPTIONS.filter((a) => a.kind === kind);
|
|
if (!assets.length) {
|
|
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;
|
|
}
|
|
const options = assets.map((a) => `<option value="${a.url}">${(a.name || '').replaceAll('<', '<')}</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() {
|
|
const url = document.getElementById('collisionWizardAssetSelect').value;
|
|
const onChoose = _collisionWizard.onAssetChosen;
|
|
_collisionWizard.onAssetChosen = null;
|
|
onChoose(url);
|
|
}
|
|
|
|
// ---------- Étape "comment l'afficher ?" (vidéo) ----------
|
|
function renderCollisionWizardVideoModeStep(onChoose) {
|
|
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) {
|
|
const onChoose = _collisionWizard.onVideoModeChosen;
|
|
_collisionWizard.onVideoModeChosen = null;
|
|
onChoose(mode);
|
|
}
|
|
|
|
// ---------- Étape "indication" (texte + durée) ----------
|
|
// L'objet ciblé a déjà été choisi juste avant (voir
|
|
// 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>';
|
|
_collisionWizard.onIndicationChosen = onChoose;
|
|
}
|
|
|
|
function collisionWizardIndicationNext() {
|
|
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 = Number.parseInt(dureeRaw, 10);
|
|
_collisionWizard.pendingIndicationObjectId = null;
|
|
const onChoose = _collisionWizard.onIndicationChosen;
|
|
_collisionWizard.onIndicationChosen = null;
|
|
onChoose(action);
|
|
}
|
|
|
|
// "dialogue"/"variable"/"surbrillance"/"visibilite"/"son"/"video"/
|
|
// "indication" (des FEUILLES, voir screens/rendering/collision_rules.py)
|
|
// enchaînent sur "Ensuite ?" (voir renderCollisionWizardChainStep —
|
|
// Marquer terminé + chaîner une autre action, demande explicite :
|
|
// "pouvoir modifier un déclencheur pour lui ajouter des actions") ;
|
|
// "interagir" enchaîne sur une carte de plus (voir
|
|
// collisionWizardFinalizeAction) ; "condition" enchaîne sur son propre
|
|
// mini-formulaire (voir renderCollisionWizardConditionStep).
|
|
function collisionWizardChooseAction(type, nested) {
|
|
if (type === 'interagir') {
|
|
collisionWizardFinalizeAction({ type }, nested);
|
|
return;
|
|
}
|
|
if (type === 'condition') {
|
|
renderCollisionWizardConditionStep(nested);
|
|
return;
|
|
}
|
|
collisionWizardBuildLeafAction(type, (leaf) => {
|
|
renderCollisionWizardChainStep(leaf, (finalLeaf) => { collisionWizardFinalizeAction(finalLeaf, nested); });
|
|
});
|
|
}
|
|
|
|
// ---------- Étape "Modifier une variable" ----------
|
|
// Même vocabulaire EXACT que l'action "Modifier une variable" du graphe
|
|
// de logique document (GLOBAL_VARIABLE_NAMES et DATA_OPERATION_LABELS_MAP
|
|
// déjà posés par templates/scene_edit.html — ce dernier n'était utilisé
|
|
// nulle part avant cette fonctionnalité) : variable ciblée, opération,
|
|
// valeur (masquée pour les opérations qui n'en ont pas besoin, voir
|
|
// COLLISION_NO_VALUE_OPERATIONS). `onChoose(action)`.
|
|
function renderCollisionWizardVariableStep(onChoose) {
|
|
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() {
|
|
const operation = document.getElementById('varWizOperation').value;
|
|
const valueInput = document.getElementById('varWizValue');
|
|
valueInput.style.display = COLLISION_NO_VALUE_OPERATIONS.has(operation) ? 'none' : '';
|
|
}
|
|
|
|
function collisionWizardVariableNext() {
|
|
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.has(operation)) {
|
|
action.data_value = document.getElementById('varWizValue').value;
|
|
}
|
|
const onChoose = _collisionWizard.onVariableChosen;
|
|
_collisionWizard.onVariableChosen = null;
|
|
onChoose(action);
|
|
}
|
|
|
|
// ---------- Étape "Ensuite ?" (chaînage + Marquer terminé) ----------
|
|
// Après CHAQUE feuille construite (dialogue ou variable, au premier
|
|
// niveau, sous "interagir", ou dans une branche de condition) : la case
|
|
// "Marquer terminé" (seulement pour un dialogue) PUIS le choix de
|
|
// chaîner une autre action à la suite (voir screens/rendering/
|
|
// collision_rules.py::"then", demande explicite "pouvoir modifier un
|
|
// déclencheur pour lui ajouter des actions") — jamais bloqué à une seule
|
|
// action par déclencheur. `onDone(headAction)` reçoit la PREMIÈRE feuille
|
|
// de la chaîne (les suivantes sont attachées via `.then`) — `headAction`
|
|
// (3e paramètre, optionnel) est la TÊTE de la chaîne, transmise SANS
|
|
// changement à travers chaque ré-appel récursif : `leafAction`, lui,
|
|
// change à chaque étape (c'est la feuille COURANTE qu'on est en train
|
|
// d'éditer/étendre). Bug corrigé : sans `headAction` distincte, "Rien de
|
|
// plus" renvoyait `_collisionWizard.chainLeaf` — qui pointe vers la
|
|
// DERNIÈRE feuille ajoutée après un premier chaînage, pas la première —
|
|
// une chaîne "attendre -> puis dialogue" ne sauvegardait donc QUE le
|
|
// dialogue, "attendre" disparaissait silencieusement.
|
|
function renderCollisionWizardChainStep(leafAction, onDone, headAction) {
|
|
headAction = headAction || leafAction;
|
|
const markCompletedChecked = leafAction.mark_completed ? ' checked' : '';
|
|
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"${markCompletedChecked}> Marquer ce dialogue comme terminé une fois joué`
|
|
+ '</label>'
|
|
: '';
|
|
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) {
|
|
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;
|
|
}
|
|
if (choice === 'none') {
|
|
onDone(headAction);
|
|
return;
|
|
}
|
|
collisionWizardBuildLeafAction(choice, (nextLeaf) => {
|
|
leafAction.then = nextLeaf; // NOSONAR S7739 - chainage d'actions (donnee), jamais une promesse/await
|
|
renderCollisionWizardChainStep(nextLeaf, onDone, headAction);
|
|
});
|
|
}
|
|
|
|
// Étape "condition" — variable/opérateur/valeur (même vocabulaire que le
|
|
// nœud Condition de l'éditeur de logique, voir screens/flow/constants.py
|
|
// ::CONDITION_OPERATORS ; GLOBAL_VARIABLE_NAMES et
|
|
// CONDITION_OPERATOR_LABELS_MAP posés par templates/scene_edit.html).
|
|
// `nested` (interagir -> condition) est reporté jusqu'à la finalisation,
|
|
// une fois les deux branches Vrai/Faux choisies ci-dessous.
|
|
function renderCollisionWizardConditionStep(nested) {
|
|
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) {
|
|
_collisionWizard.pendingCondition = {
|
|
type: 'condition',
|
|
variable: document.getElementById('condWizVariable').value,
|
|
operateur: document.getElementById('condWizOperator').value,
|
|
valeur: document.getElementById('condWizValue').value,
|
|
si_vrai: null,
|
|
si_faux: null,
|
|
};
|
|
_collisionWizard.pendingConditionNested = nested;
|
|
renderCollisionWizardBranchStep('si_vrai');
|
|
}
|
|
|
|
// Étape branche (Vrai puis Faux) — chaque branche est soit "Ne rien
|
|
// faire", soit une FEUILLE ("Déclencher un dialogue" ou "Modifier une
|
|
// variable", voir screens/rendering/collision_rules.py::
|
|
// _sanitize_condition_branch : une branche reste une feuille, jamais une
|
|
// nouvelle "interagir"/"condition" imbriquée) — les DEUX branches
|
|
// peuvent chacune déclencher LEUR PROPRE feuille (et sa propre chaîne),
|
|
// indépendamment l'une de l'autre.
|
|
function renderCollisionWizardBranchStep(branchKey) {
|
|
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) {
|
|
if (choice === 'none') {
|
|
_collisionWizard.pendingCondition[branchKey] = null;
|
|
collisionWizardBranchDone(branchKey);
|
|
return;
|
|
}
|
|
collisionWizardBuildLeafAction(choice, (leaf) => {
|
|
renderCollisionWizardChainStep(leaf, (finalLeaf) => {
|
|
_collisionWizard.pendingCondition[branchKey] = finalLeaf;
|
|
collisionWizardBranchDone(branchKey);
|
|
});
|
|
});
|
|
}
|
|
|
|
function collisionWizardBranchDone(branchKey) {
|
|
if (branchKey === 'si_vrai') {
|
|
renderCollisionWizardBranchStep('si_faux');
|
|
return;
|
|
}
|
|
const nested = _collisionWizard.pendingConditionNested;
|
|
const condition = _collisionWizard.pendingCondition;
|
|
_collisionWizard.pendingCondition = null;
|
|
collisionWizardFinalizeAction(condition, nested);
|
|
}
|