Dev #11

Merged
w-vandal merged 23 commits from dev into main 2026-09-04 19:13:42 +00:00
4 changed files with 78 additions and 24 deletions
Showing only changes of commit 00f9cf33e9 - Show all commits
@@ -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; }
});
+26 -9
View File
@@ -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);
});
}
+6 -1
View File
@@ -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
View File
@@ -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