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 '