Dev #11
@@ -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