forgeWouldCollide (personnage-controller.js) bloque le déplacement par PAS ENTIER (cmd.vitesse px/tick, 4 par défaut mais réglable) : le joueur peut donc rester bloqué jusqu'à PRESQUE un pas entier de distance de l'obstacle, pas seulement 1-3px — la marge de tolérance du correctif précédent (3px) restait insuffisante même pour la vitesse par défaut dans les cas les moins favorables. Portée à 24px, confortable pour toute vitesse raisonnablement configurée sans jamais déclencher "à la collision" alors que les objets sont encore visiblement séparés. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
222 lines
11 KiB
JavaScript
222 lines
11 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 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, et un chevauchement STRICT
|
|
// (forgeShapesOverlap nu) n'était donc jamais atteint : la règle "à la
|
|
// collision" ne se déclenchait jamais. forgeWouldCollide bloque le
|
|
// déplacement PAR PAS ENTIER (cmd.vitesse px/tick, 4 par défaut mais
|
|
// réglable plus haut dans "Commandes") : le joueur peut donc rester
|
|
// bloqué jusqu'à PRESQUE un pas entier de distance de l'obstacle, pas
|
|
// seulement 1-2px — une marge trop petite (ex. 3px) restait donc
|
|
// insuffisante pour une vitesse par défaut déjà. 24px couvre
|
|
// confortablement toute vitesse raisonnablement configurée, tout en
|
|
// restant petit face à la taille d'un sprite (souvent 64-128px) pour ne
|
|
// jamais déclencher "à la collision" alors que les objets sont encore
|
|
// visiblement séparés.
|
|
var FORGE_COLLISION_RULE_TOUCH_MARGIN = 24;
|
|
|
|
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;
|
|
}
|