// ---------- 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). 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', 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. var 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' }; // 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']; // 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) { var 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); } // ---------- 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. var _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 }; 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: screenId, trigger: null, bodyEl: 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() { var keys = _collisionWizard.target === 'screen' ? ['affichage'] : ['collision', 'clic', 'survol']; var buttons = keys.map(function (key, i) { return ''; }).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) { var types = (nested || _collisionWizard.trigger !== 'collision') ? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_WIZARD_ACTION_TYPES; var buttons = types.map(function (type, i) { return ''; }).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) { var rule = { trigger: _collisionWizard.trigger, action: 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 }; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }).then(function () { 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(function (objectId) { renderCollisionWizardValeurStep(type, function (valeur) { onBuilt({ type: type, id: collisionGenerateActionId(), object_id: objectId, valeur: valeur }); }); }); return; } if (type === 'son') { renderCollisionWizardAssetStep(function (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 }); }); }, 'video'); return; } if (type === 'indication') { renderCollisionWizardObjectTargetStep(function (objectId) { _collisionWizard.pendingIndicationObjectId = objectId; renderCollisionWizardIndicationStep(function (action) { onBuilt(action); }); }); return; } // 'attendre' renderCollisionWizardAttendreStep(function (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() { var raw = document.getElementById('attendreWizSecondes').value; var secondes = String(Math.max(0, parseFloat(raw) || 0)); var 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(function (r) { return r.json(); }) .then(function (objects) { var grid = document.getElementById('collisionWizardObjectTargetGrid'); if (!grid) return; if (!objects.length) { grid.innerHTML = '

Aucun objet éligible sur ce jeu.

'; return; } grid.innerHTML = objects.map(function (o, i) { return ''; }).join(''); _collisionWizard.onObjectTargetChosen = onChoose; }); } function collisionWizardObjectTargetChosen(objectId) { var 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) { var labelsMap = actionType === 'surbrillance' ? SURBRILLANCE_LABELS_MAP : ELEMENT_VISIBILITY_LABELS_MAP; var keys = Object.keys(labelsMap); var buttons = keys.map(function (key, i) { return ''; }).join(''); collisionWizardBodyEl().innerHTML = '

' + (actionType === 'surbrillance' ? 'Surbrillance :' : 'Visibilité :') + '

' + '
' + buttons + '
'; _collisionWizard.onValeurChosen = onChoose; } function collisionWizardValeurChosen(valeur) { var 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