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 '