// ---------- 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 // "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 ? `` : ''; } // 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) => `').join(''); collisionWizardBodyEl().innerHTML = '

Cette règle se déclenche...

' + `
${buttons}
`; } 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) => `').join(''); collisionWizardBodyEl().innerHTML = `

${nested ? 'Une fois la touche pressée...' : 'Alors...'}

` + `
${buttons}
`; } 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//triggers (objet, routes/triggers/triggers.py) // ou /game//screens//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 = '

Attendre combien de secondes avant de continuer ?

' + '' + ''; _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 = '

Sur quel objet ?

' + '
Chargement…
'; 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 = '

Aucun objet éligible sur ce jeu.

'; return; } grid.innerHTML = objects.map((o, i) => `').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) => `').join(''); collisionWizardBodyEl().innerHTML = `

${actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :'}

` + `
${buttons}
`; _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