Fix bulle "Appuie sur X" invisible + retire Attaque/Événement du picker
Bug corrigé : la bulle "Appuie sur X" était ajoutée comme ENFANT de l'objet de scène (un <img> 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
726775fe4e
commit
00f9cf33e9
@@ -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 <img> 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; }
|
||||
});
|
||||
|
||||
@@ -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 <img> 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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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 '<button type="button" class="collisionWizardChoice" style="animation-delay:' + (i * 40) + 'ms" onclick="collisionWizardChooseAction(\'' + type + '\', ' + (nested ? 'true' : 'false') + ')">' +
|
||||
|
||||
+13
-1
@@ -783,7 +783,19 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
|
||||
.collisionWizardChoice:hover{ transform:translateY(-3px) scale(1.02); border-color:var(--accent); box-shadow:0 8px 18px rgba(0,0,0,.25); }
|
||||
.collisionWizardChoice .emoji{ font-size:22px; }
|
||||
|
||||
.collisionInteractBubble{ display:none; position:absolute; left:50%; bottom:100%; transform:translateX(-50%); margin-bottom:6px; background:#111; color:#fff; border-radius:10px; padding:4px 10px; font-size:12px; white-space:nowrap; pointer-events:none; z-index:20; }
|
||||
/* Bulle "Appuie sur X" (voir forgeShowCollisionInteractBubble,
|
||||
collision-rules-controller.js) : SŒUR de l'objet dans son parent
|
||||
(.sceneWorld/.sceneUI, déjà position:relative) plutôt qu'ENFANT
|
||||
directe — un objet de scène est un <img> pour "personnage"/"decor"/
|
||||
"fond" (élément REMPLACÉ, ses enfants DOM ne sont jamais affichés).
|
||||
Sa position (left/top, en px) est calculée EN JS aux coordonnées de
|
||||
l'objet ; ce bloc ne fait que la centrer horizontalement et la lever
|
||||
au-dessus (translateY(-100%) + marge). */
|
||||
.collisionInteractBubble{
|
||||
display:none; position:absolute; transform:translate(-50%, -100%); margin-top:-6px;
|
||||
background:#111; color:#fff; border-radius:10px; padding:4px 10px; font-size:12px;
|
||||
white-space:nowrap; pointer-events:none; z-index:20;
|
||||
}
|
||||
|
||||
/* ---- Éditeur de quêtes (onglet "🗺️ Quêtes" à côté de "🧩 Collision",
|
||||
templates/scene_edit.html + modale de dialogue partagée avec
|
||||
|
||||
Reference in New Issue
Block a user