From 00f9cf33e96a2e425a5e1a69fe9eed03c1736cb5 Mon Sep 17 00:00:00 2001 From: william Date: Thu, 3 Sep 2026 12:58:00 +0200 Subject: [PATCH] =?UTF-8?q?Fix=20bulle=20"Appuie=20sur=20X"=20invisible=20?= =?UTF-8?q?+=20retire=20Attaque/=C3=89v=C3=A9nement=20du=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug corrigé : la bulle "Appuie sur X" était ajoutée comme ENFANT de l'objet de scène (un pour "personnage"/"decor"/"fond") — un élément REMPLACÉ dont les enfants DOM ajoutés en JS ne sont JAMAIS affichés par un navigateur, quel que soit son CSS. La bulle existait donc bien dans le DOM (aucune erreur) mais restait invisible à l'écran. Elle est maintenant ajoutée comme SŒUR de l'objet dans son parent (.sceneWorld/.sceneUI), positionnée en JS aux mêmes coordonnées. Assistant "+ Action" de l'éditeur de collision : "⚔️ Attaquer la cible" et "📣 Déclencher un événement" retirés des choix proposés (pas besoin pour le moment) — le serveur continue d'accepter/d'exécuter une règle déjà enregistrée avec l'un des deux, rien ne casse pour l'existant. Co-Authored-By: Claude Sonnet 5 --- .../collision-rules-controller.test.js | 46 +++++++++++++------ static/js/play/collision-rules-controller.js | 35 ++++++++++---- static/js/scenes/collision-rules-editor.js | 7 ++- static/style.css | 14 +++++- 4 files changed, 78 insertions(+), 24 deletions(-) diff --git a/static/js/play/__tests__/collision-rules-controller.test.js b/static/js/play/__tests__/collision-rules-controller.test.js index 114efed5..4bc734c9 100644 --- a/static/js/play/__tests__/collision-rules-controller.test.js +++ b/static/js/play/__tests__/collision-rules-controller.test.js @@ -20,20 +20,37 @@ const { forgeStartCollisionRuleControllers, forgeStopCollisionRuleControllers, } = require('../collision-rules-controller.js'); -function fakeEl() { - const classes = new Set(); +// La bulle "Appuie sur X" est ajoutée comme SŒUR de l'objet dans son +// PARENT (jamais enfant de l'objet lui-même — un ne peut afficher +// aucun enfant, voir forgeShowCollisionInteractBubble) : chaque +// fakeEl() partage donc un parentElement commun, comme dans le vrai DOM +// (.sceneWorld/.sceneUI). +function fakeParentEl() { const children = []; + return { + children, + appendChild(child) { children.push(child); }, + querySelector(sel) { + const m = sel.match(/data-for-object-id="(\d+)"/); + return m ? children.find(c => c._forObjectId === m[1]) : undefined; + }, + }; +} + +function fakeEl(parentEl) { + const classes = new Set(); return { style: { left: '0px', top: '0px', width: '20px', height: '20px' }, classList: { add: c => classes.add(c), remove: c => classes.delete(c), toggle: c => {}, contains: c => classes.has(c) }, - children, - appendChild(child) { children.push(child); }, - querySelector(sel) { return sel === '.collisionInteractBubble' ? children.find(c => c.className === 'collisionInteractBubble') : undefined; }, + parentElement: parentEl, }; } function fakeBubbleEl() { - return { className: 'collisionInteractBubble', style: {}, textContent: '' }; + return { + className: 'collisionInteractBubble', style: {}, textContent: '', + setAttribute(name, value) { if (name === 'data-for-object-id') this._forObjectId = String(value); }, + }; } function fakeDocument(elByObjectId) { @@ -122,24 +139,27 @@ test('tick — la collision entre le joueur et un ennemi déclenche "attaque" un test('tick — "interagir" affiche la bulle "Appuie sur [touche]" pendant la collision, puis la cache en sortant', () => { const player = { id: 1, kind: 'personnage', personnage_role: 'joueur', personnage_commandes: { interagir: 'e' } }; const pnj = { id: 3, kind: 'personnage', collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'quete', quete_id: 'q-2' } } }] }; - const playerEl = fakeEl(); - const pnjElNear = fakeEl(); + const parentNear = fakeParentEl(); + const parentFar = fakeParentEl(); + const playerElNear = fakeEl(parentNear); + const pnjElNear = fakeEl(parentNear); pnjElNear.style = { left: '5px', top: '5px', width: '20px', height: '20px' }; - const pnjElFar = fakeEl(); + const playerElFar = fakeEl(parentFar); + const pnjElFar = fakeEl(parentFar); pnjElFar.style = { left: '300px', top: '300px', width: '20px', height: '20px' }; const previousDoc = global.document; global.gameData = { personnage_animations: {} }; try { - global.document = fakeDocument({ 1: playerEl, 3: pnjElNear }); + global.document = fakeDocument({ 1: playerElNear, 3: pnjElNear }); forgeCollisionRuleTick({ elements: [player, pnj] }); - let bubble = pnjElNear.querySelector('.collisionInteractBubble'); + let bubble = parentNear.querySelector('.collisionInteractBubble[data-for-object-id="3"]'); assert.equal(bubble.style.display, 'block'); assert.equal(bubble.textContent, 'Appuie sur e'); - global.document = fakeDocument({ 1: playerEl, 3: pnjElFar }); + global.document = fakeDocument({ 1: playerElFar, 3: pnjElFar }); forgeCollisionRuleTick({ elements: [player, pnj] }); - bubble = pnjElFar.querySelector('.collisionInteractBubble'); + bubble = parentFar.querySelector('.collisionInteractBubble[data-for-object-id="3"]'); assert.equal(bubble, undefined); // jamais créée pour cet élément-là (jamais actif dessus) } finally { global.document = previousDoc; } }); diff --git a/static/js/play/collision-rules-controller.js b/static/js/play/collision-rules-controller.js index e9ae0569..e5e52d16 100644 --- a/static/js/play/collision-rules-controller.js +++ b/static/js/play/collision-rules-controller.js @@ -74,24 +74,41 @@ function forgeCollisionRuleTriggerActive(rule, objRect, playerRect) { return dist <= (rule.perimetre_px || 0); } -function forgeCollisionInteractBubbleEl(objectEl) { - var bubble = objectEl.querySelector('.collisionInteractBubble'); +// La bulle "Appuie sur X" ne peut PAS être un enfant de objectEl : un +// objet de scène (voir render_scene_object.py) est un tout simple +// pour "personnage"/"decor"/"fond" — un élément REMPLACÉ, sans modèle de +// contenu, dont les enfants ajoutés en DOM ne sont JAMAIS affichés par +// un navigateur (bug corrigé : la bulle existait bien dans le DOM mais +// restait invisible). Ajoutée comme SŒUR de objectEl (dans son parent, +// déjà position:relative — .sceneWorld/.sceneUI, voir static/style.css), +// positionnée en JS aux mêmes coordonnées que l'objet. +function forgeCollisionInteractBubbleEl(objectId, parentEl) { + var bubble = parentEl.querySelector('.collisionInteractBubble[data-for-object-id="' + objectId + '"]'); if (!bubble) { bubble = document.createElement('div'); bubble.className = 'collisionInteractBubble'; - objectEl.appendChild(bubble); + bubble.setAttribute('data-for-object-id', objectId); + parentEl.appendChild(bubble); } return bubble; } -function forgeShowCollisionInteractBubble(objectEl, key) { - var bubble = forgeCollisionInteractBubbleEl(objectEl); +function forgeShowCollisionInteractBubble(objectEl, objectId, key) { + var parentEl = objectEl.parentElement; + if (!parentEl) return; + var bubble = forgeCollisionInteractBubbleEl(objectId, parentEl); bubble.textContent = 'Appuie sur ' + key; bubble.style.display = 'block'; + var left = parseFloat(objectEl.style.left) || 0; + var top = parseFloat(objectEl.style.top) || 0; + var width = parseFloat(objectEl.style.width) || 0; + bubble.style.left = (left + width / 2) + 'px'; // centrée horizontalement (voir transform:translateX(-50%), static/style.css) + bubble.style.top = top + 'px'; // AU-DESSUS de l'objet (voir transform:translateY(-100%), static/style.css) } -function forgeHideCollisionInteractBubble(objectEl) { - var bubble = objectEl.querySelector('.collisionInteractBubble'); +function forgeHideCollisionInteractBubble(objectId, objectEl) { + var parentEl = objectEl && objectEl.parentElement; + var bubble = parentEl && parentEl.querySelector('.collisionInteractBubble[data-for-object-id="' + objectId + '"]'); if (bubble) bubble.style.display = 'none'; } @@ -141,7 +158,7 @@ function forgeCollisionRuleTick(screenData) { if (rule.action.type === 'interagir') { if (isActive) { anyInteractActive = true; - forgeShowCollisionInteractBubble(targetEl, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E'); + forgeShowCollisionInteractBubble(targetEl, obj.id, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E'); } forgeCollisionRuleActiveState[stateKey] = isActive; return; @@ -150,7 +167,7 @@ function forgeCollisionRuleTick(screenData) { if (isActive && !wasActive) forgeRunCollisionRuleAction(rule.action, targetEl, obj); forgeCollisionRuleActiveState[stateKey] = isActive; }); - if (!anyInteractActive) forgeHideCollisionInteractBubble(targetEl); + if (!anyInteractActive) forgeHideCollisionInteractBubble(obj.id, targetEl); }); } diff --git a/static/js/scenes/collision-rules-editor.js b/static/js/scenes/collision-rules-editor.js index b2ed1b68..ea2cae5a 100644 --- a/static/js/scenes/collision-rules-editor.js +++ b/static/js/scenes/collision-rules-editor.js @@ -12,6 +12,11 @@ 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) { @@ -189,7 +194,7 @@ function collisionWizardChooseTrigger(trigger) { // 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 types = nested ? COLLISION_WIZARD_ACTION_TYPES.filter(function (t) { return t !== 'interagir'; }) : COLLISION_WIZARD_ACTION_TYPES; var buttons = types.map(function (type, i) { var split = collisionSplitEmojiLabel(COLLISION_ACTION_LABELS[type]); return '