Cause réelle : un objet portant une règle de collision est presque toujours AUSSI un obstacle solide (collision activée par défaut, voir forgeWouldCollide, personnage-controller.js) — le déplacement du joueur est donc bloqué PILE au contact, sans jamais laisser les deux boîtes se chevaucher réellement. Le déclencheur "collision" testait un chevauchement STRICT (forgeShapesOverlap nu), qui n'était donc jamais atteint : la règle ne se déclenchait jamais en jeu réel, malgré un contact visible à l'écran. Le déclencheur teste maintenant la boîte de l'objet élargie d'une petite marge (3px) — assez pour détecter un simple contact — SANS toucher au blocage physique du déplacement (resté strict, inchangé). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
215 lines
10 KiB
JavaScript
215 lines
10 KiB
JavaScript
// ---------- Éditeur de collision — moteur d'exécution en jeu ----------
|
|
// Chaque objet de scène AUTRE que le joueur (jamais un "fond", jamais le
|
|
// personnage "joueur" — voir screens/rendering/collision_rules.py) peut
|
|
// porter des règles "déclencheur -> action" posées dans l'onglet
|
|
// "🧩 Collision" (static/js/scenes/collision-rules-editor.js). Ce fichier
|
|
// les FAIT VIVRE pendant le jeu : à chaque tick, teste chaque règle contre
|
|
// la position du joueur, déclenche l'action une seule fois par entrée
|
|
// (pas en continu tant que la condition reste vraie), même cadence que
|
|
// personnage-controller.js (50ms) et réutilise ses mêmes briques
|
|
// (forgeCollisionRectFromBox/forgeShapesOverlap de conditions.js).
|
|
// Pur JS client, sans le moindre appel serveur — fonctionne à l'identique
|
|
// en ligne et dans un export SCORM (voir static/js/play/offline/), aucun
|
|
// branchement FORGE_OFFLINE nécessaire ici.
|
|
|
|
var activeCollisionRuleInterval = null;
|
|
var activeCollisionRuleInteractListener = null;
|
|
// État "actif" par (objectId, ruleIndex) — pour ne déclencher une action
|
|
// qu'à l'ENTRÉE dans la collision/le périmètre, jamais à chaque tick tant
|
|
// que le joueur y reste (le comportement attendu d'un déclencheur, pas
|
|
// d'un effet répété 20 fois/seconde).
|
|
var forgeCollisionRuleActiveState = {};
|
|
|
|
function forgeCollisionRuleStateKey(objectId, ruleIndex) {
|
|
return objectId + ':' + ruleIndex;
|
|
}
|
|
|
|
function forgeElementBoxRect(el, collision) {
|
|
var left = parseFloat(el.style.left) || 0;
|
|
var top = parseFloat(el.style.top) || 0;
|
|
var width = parseFloat(el.style.width) || 0;
|
|
var height = parseFloat(el.style.height) || 0;
|
|
return forgeCollisionRectFromBox({ left: left, top: top, width: width, height: height }, collision);
|
|
}
|
|
|
|
// Une petite marge (px) appliquée SEULEMENT à la détection du
|
|
// déclencheur "collision" ci-dessous — jamais au blocage physique du
|
|
// déplacement (forgeWouldCollide, personnage-controller.js, qui reste
|
|
// strict). Bug corrigé : un objet est presque toujours aussi un
|
|
// OBSTACLE SOLIDE (collision activée par défaut) — le joueur ne peut
|
|
// donc jamais aller au-delà d'un contact tangent (bords qui se touchent
|
|
// SANS se chevaucher), et un chevauchement STRICT (forgeShapesOverlap
|
|
// nu) n'était donc jamais atteint : la règle "à la collision" ne se
|
|
// déclenchait jamais. Cette marge élargit la boîte testée pour que ce
|
|
// simple contact compte comme une collision.
|
|
var FORGE_COLLISION_RULE_TOUCH_MARGIN = 3;
|
|
|
|
function forgeExpandRect(rect, margin) {
|
|
return {
|
|
left: rect.left - margin, top: rect.top - margin,
|
|
right: rect.right + margin, bottom: rect.bottom + margin,
|
|
width: rect.width + margin * 2, height: rect.height + margin * 2,
|
|
shape: rect.shape,
|
|
};
|
|
}
|
|
|
|
// "collision" : chevauchement des boîtes, ÉLARGI d'une petite marge pour
|
|
// détecter un simple contact (voir FORGE_COLLISION_RULE_TOUCH_MARGIN
|
|
// ci-dessus). "perimetre" : distance entre les CENTRES des deux boîtes
|
|
// <= rule.perimetre_px (pas entre les bords — plus simple et suffisant
|
|
// pour une portée approximative, cohérent avec le reste du moteur qui ne
|
|
// vise pas une physique précise).
|
|
function forgeCollisionRuleTriggerActive(rule, objRect, playerRect) {
|
|
if (rule.trigger === 'collision') return forgeShapesOverlap(forgeExpandRect(objRect, FORGE_COLLISION_RULE_TOUCH_MARGIN), playerRect);
|
|
var ocx = objRect.left + objRect.width / 2, ocy = objRect.top + objRect.height / 2;
|
|
var pcx = playerRect.left + playerRect.width / 2, pcy = playerRect.top + playerRect.height / 2;
|
|
var dist = Math.hypot(ocx - pcx, ocy - pcy);
|
|
return dist <= (rule.perimetre_px || 0);
|
|
}
|
|
|
|
function forgeCollisionInteractBubbleEl(objectEl) {
|
|
var bubble = objectEl.querySelector('.collisionInteractBubble');
|
|
if (!bubble) {
|
|
bubble = document.createElement('div');
|
|
bubble.className = 'collisionInteractBubble';
|
|
objectEl.appendChild(bubble);
|
|
}
|
|
return bubble;
|
|
}
|
|
|
|
function forgeShowCollisionInteractBubble(objectEl, key) {
|
|
var bubble = forgeCollisionInteractBubbleEl(objectEl);
|
|
bubble.textContent = 'Appuie sur ' + key;
|
|
bubble.style.display = 'block';
|
|
}
|
|
|
|
function forgeHideCollisionInteractBubble(objectEl) {
|
|
var bubble = objectEl.querySelector('.collisionInteractBubble');
|
|
if (bubble) bubble.style.display = 'none';
|
|
}
|
|
|
|
// Exécute une action (top-level ou sub_action d'un "interagir") — jamais
|
|
// appelée pour un "interagir" lui-même à ce niveau, voir forgeCollisionRuleTick
|
|
// qui gère "interagir" à part (affichage de bulle + attente de touche).
|
|
function forgeRunCollisionRuleAction(action, targetEl, obj) {
|
|
if (action.type === 'evenement') {
|
|
window.dispatchGameEvent(action.evenement_id);
|
|
return;
|
|
}
|
|
if (action.type === 'attaque') {
|
|
var attackAnim = forgePersonnageAnimExists(obj.id, 'attaque') ? 'attaque' : null;
|
|
if (attackAnim) runSpriteAnimation(targetEl, resolveSpriteFrames(obj.id, { animation: attackAnim, fps: FORGE_PERSONNAGE_FPS, loop: false }));
|
|
return;
|
|
}
|
|
if (action.type === 'quete') {
|
|
// Affiche la réplique en cours du dialogue de cette quête, selon son
|
|
// statut actuel, dans le widget "💬 Boîte de dialogue" posé sur
|
|
// l'écran (voir static/js/play/dialogue-box-controller.js) — sans
|
|
// effet si aucun widget n'est posé ou si la quête n'a aucune
|
|
// réplique pour son statut, comme le reste de ce moteur.
|
|
forgeShowDialogueBox(action.quete_id);
|
|
return;
|
|
}
|
|
}
|
|
|
|
function forgeCollisionRuleTick(screenData) {
|
|
var player = (screenData.elements || []).find(function (o) { return o.kind === 'personnage' && o.personnage_role === 'joueur'; });
|
|
var playerEl = player && document.querySelector('[data-object-id="' + player.id + '"]');
|
|
if (!player || !playerEl) return;
|
|
var playerRect = forgeElementBoxRect(playerEl, player.collision);
|
|
|
|
(screenData.elements || []).forEach(function (obj) {
|
|
var rules = obj.collision_rules;
|
|
if (!rules || !rules.length) return;
|
|
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
|
|
if (!targetEl) return;
|
|
var objRect = forgeElementBoxRect(targetEl, obj.collision);
|
|
|
|
var anyInteractActive = false;
|
|
rules.forEach(function (rule, ruleIndex) {
|
|
var stateKey = forgeCollisionRuleStateKey(obj.id, ruleIndex);
|
|
var isActive = forgeCollisionRuleTriggerActive(rule, objRect, playerRect);
|
|
var wasActive = !!forgeCollisionRuleActiveState[stateKey];
|
|
|
|
if (rule.action.type === 'interagir') {
|
|
if (isActive) {
|
|
anyInteractActive = true;
|
|
forgeShowCollisionInteractBubble(targetEl, (player.personnage_commandes && player.personnage_commandes.interagir) || 'E');
|
|
}
|
|
forgeCollisionRuleActiveState[stateKey] = isActive;
|
|
return;
|
|
}
|
|
|
|
if (isActive && !wasActive) forgeRunCollisionRuleAction(rule.action, targetEl, obj);
|
|
forgeCollisionRuleActiveState[stateKey] = isActive;
|
|
});
|
|
if (!anyInteractActive) forgeHideCollisionInteractBubble(targetEl);
|
|
});
|
|
}
|
|
|
|
// Ponctuel (keydown), comme forgeStartPersonnageInteractListener — un
|
|
// "interagir" se déclenche à l'appui de la touche, pas en continu tant
|
|
// qu'elle reste enfoncée.
|
|
function forgeStartCollisionRuleInteractListener(screenData) {
|
|
activeCollisionRuleInteractListener = function (ev) {
|
|
var player = (screenData.elements || []).find(function (o) { return o.kind === 'personnage' && o.personnage_role === 'joueur'; });
|
|
if (!player || !player.personnage_commandes || ev.key !== player.personnage_commandes.interagir) return;
|
|
(screenData.elements || []).forEach(function (obj) {
|
|
var rules = obj.collision_rules;
|
|
if (!rules || !rules.length) return;
|
|
var targetEl = document.querySelector('[data-object-id="' + obj.id + '"]');
|
|
if (!targetEl) return;
|
|
rules.forEach(function (rule, ruleIndex) {
|
|
if (rule.action.type !== 'interagir') return;
|
|
var stateKey = forgeCollisionRuleStateKey(obj.id, ruleIndex);
|
|
if (!forgeCollisionRuleActiveState[stateKey]) return;
|
|
forgeRunCollisionRuleAction(rule.action.sub_action, targetEl, obj);
|
|
});
|
|
});
|
|
};
|
|
window.addEventListener('keydown', activeCollisionRuleInteractListener);
|
|
}
|
|
|
|
function forgeStopCollisionRuleControllers() {
|
|
if (activeCollisionRuleInterval) {
|
|
clearInterval(activeCollisionRuleInterval);
|
|
activeCollisionRuleInterval = null;
|
|
}
|
|
if (activeCollisionRuleInteractListener) {
|
|
window.removeEventListener('keydown', activeCollisionRuleInteractListener);
|
|
activeCollisionRuleInteractListener = null;
|
|
}
|
|
forgeCollisionRuleActiveState = {};
|
|
}
|
|
|
|
// Même patron par écran que forgeStartPersonnageControllers — appelé
|
|
// juste après lui (voir static/js/play/screens.js), n'a d'effet que si
|
|
// l'écran a un joueur ET au moins un objet avec des règles.
|
|
function forgeStartCollisionRuleControllers(screenData) {
|
|
forgeStopCollisionRuleControllers();
|
|
if (!screenData || screenData.kind !== 'jeu_2d') return;
|
|
var elements = screenData.elements || [];
|
|
var hasPlayer = elements.some(function (o) { return o.kind === 'personnage' && o.personnage_role === 'joueur'; });
|
|
var hasRules = elements.some(function (o) { return o.collision_rules && o.collision_rules.length; });
|
|
if (!hasPlayer || !hasRules) return;
|
|
activeCollisionRuleInterval = setInterval(function () { forgeCollisionRuleTick(screenData); }, 50);
|
|
forgeStartCollisionRuleInteractListener(screenData);
|
|
}
|
|
|
|
// static/js/play/__tests__/ (node:test) — même garde que personnage-controller.js.
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
module.exports = {
|
|
forgeCollisionRuleTriggerActive, forgeRunCollisionRuleAction, forgeCollisionRuleTick,
|
|
forgeStartCollisionRuleControllers, forgeStopCollisionRuleControllers,
|
|
forgeCollisionRuleStateKey, forgeElementBoxRect, forgeExpandRect,
|
|
};
|
|
const conditionsMod = require('./conditions.js');
|
|
global.forgeCollisionRectFromBox = global.forgeCollisionRectFromBox || conditionsMod.forgeCollisionRectFromBox;
|
|
global.forgeShapesOverlap = global.forgeShapesOverlap || conditionsMod.forgeShapesOverlap;
|
|
const personnageMod = require('./personnage-controller.js');
|
|
global.forgePersonnageAnimExists = global.forgePersonnageAnimExists || personnageMod.forgePersonnageAnimExists;
|
|
global.FORGE_PERSONNAGE_FPS = global.FORGE_PERSONNAGE_FPS || 8;
|
|
const dialogueBoxMod = require('./dialogue-box-controller.js');
|
|
global.forgeShowDialogueBox = global.forgeShowDialogueBox || dialogueBoxMod.forgeShowDialogueBox;
|
|
}
|