// ---------- É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', }; // "attaque"/"evenement" retirĂ©s de l'assistant (pas besoin pour le // moment, demande explicite) — server-side (screens/rendering/ // collision_rules.py::ACTION_TYPES) les accepte encore telles quelles, // pour ne jamais casser une rĂšgle dĂ©jĂ  enregistrĂ©e avec l'un des deux. var COLLISION_WIZARD_ACTION_TYPES = ['quete', 'interagir']; 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') { var quest = (typeof questById === 'function') ? questById(action.quete_id) : null; return COLLISION_ACTION_LABELS.quete + ' « ' + (quest ? quest.title : '#' + 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" en forme de piĂšces qui s'emboĂźtent // (dĂ©clencheur -> action -> sous-action Ă©ventuelle), pas une liste plate — // chaque maillon porte un data-kind (couleur en CSS) et un dĂ©lai d'entrĂ©e // en cascade (voir @keyframes collisionLinkIn, static/style.css). function collisionRuleChainHtml(rule, ruleIndex, objectId) { var links = [ { kind: 'trigger', text: collisionRuleTriggerSummary(rule) }, { kind: rule.action.type, text: collisionRuleActionSummary(rule.action), questId: rule.action.quete_id }, ]; var action = rule.action; while (action.type === 'interagir' && action.sub_action) { links.push({ kind: action.sub_action.type, text: collisionRuleActionSummary(action.sub_action), questId: action.sub_action.quete_id }); action = action.sub_action; } // Un maillon "quete" (kind==='quete') ouvre directement sa modale de // dialogue (voir openQuestDialogueModal(), static/js/quests/ // quest-editor.js) au clic — demande explicite, plutĂŽt qu'un simple // texte informatif comme les autres maillons. Reste un comme les // autres (jamais un ' + '' ); } // 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 120×120 de cette miniature : la rĂšgle CSS // `.collisionRuleCardThumb img` (voir static/style.css) est censĂ©e le // neutraliser (position:static !important etc.), 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. function collisionRuleThumbHtml(obj) { var match = /\ssrc="([^"]*)"/.exec(obj.thumbnail_html || ''); return match ? '' : ''; } 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 ( '
' + '
' + '
' + collisionRuleThumbHtml(obj) + '
' + '

' + 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; } // Chaque carte entre en cascade (voir @keyframes collisionCardIn, // static/style.css) plutĂŽt que toutes en mĂȘme temps. container.innerHTML = COLLISION_RULE_OBJECTS.map(function (obj, i) { return collisionRuleCardHtml(obj).replace('
' + '' + '' + '
'; } 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_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); } 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" ouvre l'Ă©diteur de quĂȘte lui-mĂȘme — voir // static/js/quests/quest-editor.js::renderQuestPickerStep, pas un simple // champ texte ; Ă©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') { renderQuestPickerStep(function (quest) { collisionWizardFinalizeAction({ type: 'quete', quete_id: quest.id }, nested); }); 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, i) { return ''; }).join(''); collisionWizardBodyEl().innerHTML = '

Quel événement ?

' + buttons + '
'; } } // Appelé directement (pas de DOMContentLoaded) : ce fichier est chargé par // un