// ---------- Éditeur de collision (nouvel onglet "🧩 Collision") ---------- // Une carte par objet ÉLIGIBLE (jamais un "fond", jamais le personnage // "joueur" — voir COLLISION_RULE_OBJECTS, construit par routes/scenes/ // scene_edit_view.py), listant ses règles "déclencheur -> action" déjà // posées + un bouton "+ Action" qui ouvre un assistant pas à pas (jamais // un vrai formulaire à plusieurs champs) : un clic choisit une option, la // carte suivante s'emboîte aussitôt. 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 = { quete: '🗺️ Déclencher une quête', attaque: '⚔️ Attaquer la cible', evenement: '📣 Déclencher un événement', interagir: '🔑 Interagir avec une touche', }; var COLLISION_TRIGGER_LABELS = { collision: '💥 À la collision', perimetre: '📍 Dans un périmètre' }; function collisionRuleObjectLabel(obj) { var kindLabel = obj.kind === 'personnage' ? (obj.role === 'ennemie' ? 'Ennemi' : 'PNJ') : 'Décor'; return (obj.name || kindLabel); } // Un maillon = un segment de la "chaîne de pièces" affichée dans la carte // (déclencheur, puis action, puis éventuellement sous-action) — texte // seul, la logique de lecture vit dans collisionRuleActionSummary/ // collisionRuleTriggerSummary ci-dessous. function collisionRuleTriggerSummary(rule) { if (rule.trigger === 'perimetre') return COLLISION_TRIGGER_LABELS.perimetre + ' (' + rule.perimetre_px + 'px)'; return COLLISION_TRIGGER_LABELS.collision; } function collisionRuleActionSummary(action) { if (action.type === 'quete') return COLLISION_ACTION_LABELS.quete + ' « ' + action.quete_id + ' »'; if (action.type === 'evenement') { var ev = CUSTOM_EVENTS_MAP[action.evenement_id]; return COLLISION_ACTION_LABELS.evenement + ' « ' + (ev ? ev.name : '?') + ' »'; } return COLLISION_ACTION_LABELS[action.type] || action.type; } // Une règle = une chaîne de "maillons" (déclencheur -> action -> sous-action // éventuelle) — chacun sa propre petite carte, visuellement emboîtées. function collisionRuleChainHtml(rule, ruleIndex, objectId) { var links = [collisionRuleTriggerSummary(rule), collisionRuleActionSummary(rule.action)]; var action = rule.action; while (action.type === 'interagir' && action.sub_action) { links.push(collisionRuleActionSummary(action.sub_action)); action = action.sub_action; } var linksHtml = links.map(function (text) { return '' + text.replace(/'; }).join('→'); return ( '
' + '' + '' + '
' ); } function collisionRuleCardHtml(obj) { var rulesHtml = obj.rules.length ? obj.rules.map(function (rule, i) { return collisionRuleChainHtml(rule, i, obj.id); }).join('') : '

Aucune règle pour l\'instant.

'; return ( '
' + '
' + '
' + obj.thumbnail_html + '
' + '

' + collisionRuleObjectLabel(obj).replace(/' + '

' + '
' + rulesHtml + '
' + '' + '
' ); } function renderCollisionRuleCards() { var container = document.getElementById('collisionRulesCards'); if (!container) return; if (!COLLISION_RULE_OBJECTS.length) { container.innerHTML = '

Aucun objet éligible pour l\'instant — un décor, un ennemi ou un PNJ (jamais le joueur lui-même).

'; return; } container.innerHTML = COLLISION_RULE_OBJECTS.map(collisionRuleCardHtml).join(''); } function collisionRuleObjectById(objectId) { return COLLISION_RULE_OBJECTS.find(function (o) { return o.id === objectId; }); } // Enregistre la liste COMPLÈTE des règles de cet objet (voir // routes/scenes/scene_object_collision_rules.py — toujours la liste // entière, jamais une règle isolée) puis redessine SA carte. function saveCollisionRules(objectId) { var obj = collisionRuleObjectById(objectId); if (!obj) return; fetch('/game/' + GAME_SLUG + '/scene-objects/' + objectId + '/collision-rules', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ rules: obj.rules }), }) .then(function (r) { return r.json(); }) .then(function (json) { if (json.rules) obj.rules = json.rules; // reflète la validation serveur (voir sanitize_collision_rules) renderCollisionRuleCards(); }); } function deleteCollisionRule(objectId, ruleIndex) { var obj = collisionRuleObjectById(objectId); if (!obj) return; obj.rules.splice(ruleIndex, 1); saveCollisionRules(objectId); } // ---------- Assistant "+ 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"). var _collisionWizard = null; function openCollisionRuleModal(objectId) { _collisionWizard = { objectId: objectId, trigger: null, perimetrePx: COLLISION_DEFAULT_PERIMETRE_PX }; document.getElementById('collisionRuleModal').classList.add('is-active'); renderCollisionWizardTriggerStep(); } function closeCollisionRuleModal() { document.getElementById('collisionRuleModal').classList.remove('is-active'); _collisionWizard = null; } function collisionWizardBodyEl() { return document.getElementById('collisionRuleModalBody'); } // Étape 1 — déclencheur : collision directe, ou périmètre (px réglable, // valeur par défaut COLLISION_DEFAULT_PERIMETRE_PX). function renderCollisionWizardTriggerStep() { collisionWizardBodyEl().innerHTML = '

Cette règle se déclenche...

' + '
' + '' + '
' + '' + '' + 'px' + '
' + '
'; } function collisionWizardChooseTrigger(trigger) { _collisionWizard.trigger = trigger; if (trigger === 'perimetre') { var pxEl = document.getElementById('collisionWizardPerimetrePx'); _collisionWizard.perimetrePx = (pxEl && parseFloat(pxEl.value)) || COLLISION_DEFAULT_PERIMETRE_PX; } 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. function renderCollisionWizardActionStep(nested) { var types = nested ? COLLISION_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_ACTION_TYPES; var buttons = types.map(function (type) { 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); } function collisionWizardSaveRule(action) { var rule = { trigger: _collisionWizard.trigger, action: action }; if (_collisionWizard.trigger === 'perimetre') rule.perimetre_px = _collisionWizard.perimetrePx; var obj = collisionRuleObjectById(_collisionWizard.objectId); obj.rules.push(rule); var objectId = _collisionWizard.objectId; closeCollisionRuleModal(); saveCollisionRules(objectId); } // Chaque type d'action demande éventuellement une précision avant de // pouvoir se finaliser (quête -> son id ; événement -> lequel) — "attaque" // n'a besoin de rien de plus, se finalise directement. function collisionWizardChooseAction(type, nested) { if (type === 'attaque' || type === 'interagir') { collisionWizardFinalizeAction({ type: type }, nested); return; } if (type === 'quete') { collisionWizardBodyEl().innerHTML = '

Identifiant de la quête (l\'éditeur de quête reste à construire — cette règle ne fait que mémoriser la référence pour l\'instant) :

' + '
' + '
' + '
' + '
'; return; } if (type === 'evenement') { var eventIds = Object.keys(CUSTOM_EVENTS_MAP); if (!eventIds.length) { collisionWizardBodyEl().innerHTML = '

Aucun événement personnalisé pour l\'instant — crée-en un dans l\'onglet "📣 Événements", puis reviens ici.

'; return; } var buttons = eventIds.map(function (id) { return ''; }).join(''); collisionWizardBodyEl().innerHTML = '

Quel événement ?

' + buttons + '
'; } } function collisionWizardSubmitQuete(nested) { var input = document.getElementById('collisionWizardQueteId'); var queteId = input && input.value.trim(); if (!queteId) { input.focus(); return; } collisionWizardFinalizeAction({ type: 'quete', quete_id: queteId }, nested); } document.addEventListener('DOMContentLoaded', function () { if (typeof COLLISION_RULE_OBJECTS !== 'undefined') renderCollisionRuleCards(); });