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