diff --git a/static/js/play/__tests__/collision-rules-controller.test.js b/static/js/play/__tests__/collision-rules-controller.test.js index 4bc734c9..d11e01ec 100644 --- a/static/js/play/__tests__/collision-rules-controller.test.js +++ b/static/js/play/__tests__/collision-rules-controller.test.js @@ -136,6 +136,38 @@ test('tick — la collision entre le joueur et un ennemi déclenche "attaque" un } finally { global.document = previousDoc; } }); +test('tick — la bulle "Appuie sur X" s\'ancre sur la BOÎTE DE COLLISION, pas le cadre brut du sprite', () => { + // Bug corrigé ("c'est trop haut") : un sprite très padé (animaux + // CraftPix...) a un cadre bien plus grand que sa silhouette visible — + // ancrer la bulle sur ce cadre brut la faisait flotter loin au-dessus + // du personnage. objRect (issu de forgeElementBoxRect, qui applique + // déjà collision.width/height/offset_x/offset_y) doit être utilisé. + const player = { id: 1, kind: 'personnage', personnage_role: 'joueur' }; + const pnj = { + id: 3, kind: 'personnage', + // Sprite de 100×200 px, mais silhouette réelle 20×20 centrée en bas + // (offset_y positif = décalée vers le bas, voir collision_settings.py). + collision: { enabled: true, shape: 'rectangle', width: 20, height: 20, offset_x: 0, offset_y: 80 }, + collision_rules: [{ trigger: 'collision', action: { type: 'interagir', sub_action: { type: 'quete', quete_id: 1 } } }], + }; + const parent = fakeParentEl(); + const playerEl = fakeEl(parent); + playerEl.style = { left: '45px', top: '175px', width: '20px', height: '20px' }; // chevauche la silhouette réelle du pnj (40,170)-(60,190) + const pnjEl = fakeEl(parent); + pnjEl.style = { left: '0px', top: '0px', width: '100px', height: '200px' }; // grand cadre padé + + const previousDoc = global.document; + global.gameData = { personnage_animations: {} }; + try { + global.document = fakeDocument({ 1: playerEl, 3: pnjEl }); + forgeCollisionRuleTick({ elements: [player, pnj] }); + const bubble = parent.querySelector('.collisionInteractBubble[data-for-object-id="3"]'); + // Boîte de collision : left = (100-20)/2+0 = 40, top = (200-20)/2+80 = 170. + assert.equal(bubble.style.top, '170px'); // PAS "0px" (le haut du cadre brut) + assert.equal(bubble.style.left, '50px'); // centrée sur la boîte de collision (40 + 20/2) + } finally { global.document = previousDoc; } +}); + 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' } } }] }; diff --git a/static/js/play/collision-rules-controller.js b/static/js/play/collision-rules-controller.js index e5e52d16..f1cc605f 100644 --- a/static/js/play/collision-rules-controller.js +++ b/static/js/play/collision-rules-controller.js @@ -93,17 +93,23 @@ function forgeCollisionInteractBubbleEl(objectId, parentEl) { return bubble; } -function forgeShowCollisionInteractBubble(objectEl, objectId, key) { +// Positionnée sur la BOÎTE DE COLLISION (objRect, déjà résolue via +// forgeElementBoxRect — largeur/hauteur/décalage réglables dans "🧱 +// Collision") plutôt que sur le cadre brut du : beaucoup de +// sprites (notamment les animaux CraftPix, voir screens/labels/ +// animal_sprite_library.py) ont un TRÈS GRAND canevas très padé autour +// d'une silhouette bien plus petite — ancrée sur le cadre brut, la +// bulle flottait loin au-dessus du personnage visible (bug corrigé : +// "c'est trop haut"). La boîte de collision, que l'auteur ajuste déjà +// pour épouser la silhouette réelle, donne une ancre bien plus fidèle. +function forgeShowCollisionInteractBubble(objectEl, objectId, objRect, 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) + bubble.style.left = (objRect.left + objRect.width / 2) + 'px'; // centrée horizontalement (voir transform:translateX(-50%), static/style.css) + bubble.style.top = objRect.top + 'px'; // AU-DESSUS de la boîte de collision (voir transform:translateY(-100%), static/style.css) } function forgeHideCollisionInteractBubble(objectId, objectEl) { @@ -158,7 +164,7 @@ function forgeCollisionRuleTick(screenData) { if (rule.action.type === 'interagir') { if (isActive) { anyInteractActive = true; - forgeShowCollisionInteractBubble(targetEl, obj.id, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E'); + forgeShowCollisionInteractBubble(targetEl, obj.id, objRect, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E'); } forgeCollisionRuleActiveState[stateKey] = isActive; return;