Build and deploy / test-python (push) Successful in 11m12s
Build and deploy / test-js (push) Successful in 53s
Build and deploy / lint-python (push) Successful in 3m56s
Build and deploy / lint-js (push) Successful in 3m1s
Build and deploy / build-and-push (push) Skipped
Build and deploy / deploy (push) Skipped
Build and deploy / sonarqube (push) Failing after 3m58s
Prepare la scission a venir entre l'editeur Jeu 2D et le futur editeur Support de formation (voir docs/plan/PLAN.md), sans toucher a l'architecture en couches existante : - screens/ renomme en game_engine/ (nom clair pour le moteur du jeu 2D, avant l'arrivee d'un second "moteur" cote document) : ~85 imports corriges, contrat import-linter mis a jour, meme forme de couches. - routes/, scripts/, static/, templates/, tests/ : tout ce qui est propre au jeu 2D deplace dans un sous-dossier game/ de chacun (routes/game/, static/game/, templates/game/, tests/game/, scripts/game/) ; ce qui est partage par le site (auth, onboarding, dashboard, uploads, db/) reste a la racine de chaque dossier. Un sous-dossier document/ (vide) cree dans chacun pour le futur chantier. - styles/ volontairement inchange : les 3 fichiers sources sont concatenes en un seul static/style.css charge par tout le site, scinder leur CONTENU (editeur vs partage) serait un refactor CSS distinct, pas un deplacement mecanique. - Chaine d'export SCORM (publish/build_scorm_package.py) mise a jour en profondeur : copie des assets, URLs d'icones relatives a static/style.css (qui ne bouge pas), manifeste, wrapper SCORM. - Deux regressions d'un sweep de renommage anterieur corrigees au passage (screens.js/screens/scene-objects incorrectement convertis en game_engine.js/game_engine/scene-objects dans des commentaires). - Effet de bord Windows decouvert et corrige : git mv + Path.write_text convertissent des fichiers en CRLF (core.autocrlf=true) - ~189 fichiers normalises en LF. - .eslintrc.json/package.json : uniquement les chemins de glob mis a jour (static/game/js/...) ; la preparation eslint-plugin-unicorn du lot 7 reste volontairement non committee (package-lock.json restaure a la version precedente). Verifications : ruff, mypy --strict (391 fichiers), vulture, bandit, lint-imports tous verts ; 591/591 tests Python, 276/276 tests JS ; demarrage serveur + requetes HTTP manuelles confirmant que les assets deplaces repondent en 200 au nouvel emplacement et 404 a l'ancien. SKIP=djlint : backlog H021 (styles inline) deja documente comme dette assumee dans CODE_QUALITY.md section 6, aucun template touche par ce commit au-dela d'un deplacement de fichier. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
418 lines
20 KiB
JavaScript
418 lines
20 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 game_engine/rendering/collision_rules.py) peut
|
||
// porter des règles "déclencheur -> action", posées depuis l'onglet
|
||
// "Déclencheurs" (static/game/js/triggers/trigger-editor.js::openTriggerWizard).
|
||
// 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/game/js/play/offline/), aucun
|
||
// branchement FORGE_OFFLINE nécessaire ici.
|
||
|
||
let activeCollisionRuleInterval = null;
|
||
let 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).
|
||
let forgeCollisionRuleActiveState = {};
|
||
|
||
function forgeCollisionRuleStateKey(objectId, ruleIndex) {
|
||
return `${objectId}:${ruleIndex}`;
|
||
}
|
||
|
||
function forgeElementBoxRect(el, collision) {
|
||
const left = Number.parseFloat(el.style.left) || 0;
|
||
const top = Number.parseFloat(el.style.top) || 0;
|
||
const width = Number.parseFloat(el.style.width) || 0;
|
||
const height = Number.parseFloat(el.style.height) || 0;
|
||
return forgeCollisionRectFromBox({
|
||
left, top, width, 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.
|
||
const 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) — seul déclencheur encore proposé ("perimetre" retiré, ne
|
||
// servait à rien pour l'instant, demande explicite).
|
||
function forgeCollisionRuleTriggerActive(rule, objRect, playerRect) {
|
||
return forgeShapesOverlap(forgeExpandRect(objRect, FORGE_COLLISION_RULE_TOUCH_MARGIN), playerRect);
|
||
}
|
||
|
||
// 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) {
|
||
let bubble = parentEl.querySelector(`.collisionInteractBubble[data-for-object-id="${objectId}"]`);
|
||
if (!bubble) {
|
||
bubble = document.createElement('div');
|
||
bubble.className = 'collisionInteractBubble';
|
||
bubble.dataset.forObjectId = objectId;
|
||
parentEl.appendChild(bubble);
|
||
}
|
||
return bubble;
|
||
}
|
||
|
||
// Positionnée sur la BOÎTE DE COLLISION (objRect, déjà résolue via
|
||
// forgeElementBoxRect — largeur/hauteur/décalage réglables dans "🧱
|
||
// Collision") plutôt que sur le cadre brut du <img> : beaucoup de
|
||
// sprites (notamment les animaux CraftPix, voir game_engine/labels/
|
||
// animal_sprite_library.py) ont un TRÈS GRAND canevas très padé autour
|
||
// d'une silhouette bien plus petite — ancrée sur le cadre brut, la
|
||
// bulle flottait loin au-dessus du personnage visible (bug corrigé :
|
||
// "c'est trop haut"). La boîte de collision, que l'auteur ajuste déjà
|
||
// pour épouser la silhouette réelle, donne une ancre bien plus fidèle.
|
||
function forgeShowCollisionInteractBubble(objectEl, objectId, objRect, key) {
|
||
const parentEl = objectEl.parentElement;
|
||
if (!parentEl) return;
|
||
const bubble = forgeCollisionInteractBubbleEl(objectId, parentEl);
|
||
bubble.textContent = `Appuie sur ${key}`;
|
||
bubble.style.display = 'block';
|
||
bubble.style.left = `${objRect.left + objRect.width / 2}px`; // centrée horizontalement (voir transform:translateX(-50%), static/style.css)
|
||
bubble.style.top = `${objRect.top}px`; // AU-DESSUS de la boîte de collision (voir transform:translateY(-100%), static/style.css)
|
||
}
|
||
|
||
function forgeHideCollisionInteractBubble(objectId, objectEl) {
|
||
const parentEl = objectEl?.parentElement;
|
||
const bubble = parentEl?.querySelector(`.collisionInteractBubble[data-for-object-id="${objectId}"]`);
|
||
if (bubble) bubble.style.display = 'none';
|
||
}
|
||
|
||
// ---------- Action "indication" — bulle de texte libre, temporisée ----------
|
||
// Distincte de la bulle "Appuie sur X" ci-dessus (sa propre classe CSS,
|
||
// sa propre minuterie) : ne dépend jamais du cycle "actif tant que la
|
||
// collision dure" d'un déclencheur "interagir", peut être posée sur
|
||
// N'IMPORTE QUEL objet de l'écran (voir action.object_id, pas
|
||
// nécessairement l'objet qui porte le déclencheur).
|
||
const FORGE_INDICATION_DEFAULT_DURATION_MS = 3000;
|
||
const forgeIndicationTimeouts = {};
|
||
|
||
function forgeIndicationBubbleEl(objectId, parentEl) {
|
||
let bubble = parentEl.querySelector(`.collisionIndicationBubble[data-for-object-id="${objectId}"]`);
|
||
if (!bubble) {
|
||
bubble = document.createElement('div');
|
||
bubble.className = 'collisionIndicationBubble';
|
||
bubble.dataset.forObjectId = objectId;
|
||
parentEl.appendChild(bubble);
|
||
}
|
||
return bubble;
|
||
}
|
||
|
||
function forgeShowIndicationBubble(objectId, texte, dureeMs) {
|
||
const targetEl = document.querySelector(`[data-object-id="${objectId}"]`);
|
||
const parentEl = targetEl?.parentElement;
|
||
if (!targetEl || !parentEl || !texte) return;
|
||
const objRect = forgeElementBoxRect(targetEl);
|
||
const bubble = forgeIndicationBubbleEl(objectId, parentEl);
|
||
bubble.textContent = texte;
|
||
bubble.style.display = 'block';
|
||
bubble.style.left = `${objRect.left + objRect.width / 2}px`;
|
||
bubble.style.top = `${objRect.top}px`;
|
||
if (forgeIndicationTimeouts[objectId]) clearTimeout(forgeIndicationTimeouts[objectId]);
|
||
forgeIndicationTimeouts[objectId] = setTimeout(() => {
|
||
bubble.style.display = 'none';
|
||
}, dureeMs || FORGE_INDICATION_DEFAULT_DURATION_MS);
|
||
}
|
||
|
||
// Affiche LE DIALOGUE DE CETTE ACTION PRÉCISE (action.dialogue.lines —
|
||
// chaque occurrence de "Déclencher ce dialogue" porte le sien, voir
|
||
// game_engine/rendering/collision_rules.py) dans le widget "💬 Boîte de
|
||
// dialogue" posé sur l'écran (voir static/game/js/play/dialogue-box-
|
||
// controller.js) — sans effet si aucun widget n'est posé, comme le reste
|
||
// de ce moteur. Le nom affiché (en-tête du quiz, xAPI) est celui de
|
||
// l'objet déclencheur (voir "ℹ️ Informations") — plus de "quête" pour le
|
||
// porter. mark_completed (optionnel) : marque ce dialogue terminé une
|
||
// fois épuisé. `action.then` (chaînage, voir maquette validée : "pouvoir
|
||
// modifier un déclencheur pour lui ajouter des actions") NE PEUT PAS
|
||
// s'exécuter immédiatement — un dialogue attend des clics du joueur —
|
||
// transmis à forgeShowDialogueBox, exécuté par dialogue-box-controller.js
|
||
// une fois le dialogue épuisé.
|
||
function runDialogueCollisionAction(action, targetEl, obj) {
|
||
const lines = action.dialogue?.lines || [];
|
||
const dialogueId = action.dialogue?.id;
|
||
forgeShowDialogueBox(dialogueId, obj.name || 'PNJ', lines, action.mark_completed, action.then);
|
||
}
|
||
|
||
// Modifie une variable globale — même vocabulaire/calcul que l'action
|
||
// "Modifier une variable" du graphe de logique document (voir
|
||
// game_engine/data_actions/apply_variable_action.py), portée ici par
|
||
// forgeApplyVariableActionOffline (static/game/js/play/offline/apply-actions.js,
|
||
// TOUJOURS chargée — voir templates/game/play.html — pas seulement en export,
|
||
// donc aucun branchement FORGE_OFFLINE nécessaire, même philosophie que
|
||
// le reste de ce fichier). Instantané : `then` s'exécute tout de suite,
|
||
// contrairement à un dialogue.
|
||
function runVariableCollisionAction(action, targetEl, obj) {
|
||
forgeApplyVariableActionOffline(gameData, action);
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
}
|
||
|
||
// Réutilise evaluateConditionClause (static/game/js/play/conditions.js), même
|
||
// comparaison qu'un nœud Condition de l'éditeur de logique, sur une
|
||
// VARIABLE GLOBALE (gameData.variables) — jamais un champ d'objet ici
|
||
// (voir maquette validée : le losange ne teste qu'une variable).
|
||
function runConditionCollisionAction(action, targetEl, obj) {
|
||
const isTrue = evaluateConditionClause({
|
||
source: 'variable', variable: action.variable, operator: action.operateur, value: action.valeur,
|
||
});
|
||
const branch = isTrue ? action.si_vrai : action.si_faux;
|
||
if (branch) forgeRunCollisionRuleAction(branch, targetEl, obj);
|
||
}
|
||
|
||
// Cible N'IMPORTE QUEL objet de l'écran (action.object_id, pas forcément
|
||
// celui qui porte le déclencheur) — réutilise TEL QUEL applyObjectProperty
|
||
// (static/game/js/play/actions.js, équivalent scène 2D d'applyElementProperty),
|
||
// plutôt que de dupliquer cette logique. Partagée entre "surbrillance" et
|
||
// "visibilite", qui suivent exactement le même patron.
|
||
function runSurbrillanceOuVisibiliteCollisionAction(action, targetEl, obj) {
|
||
const targetObjEl = document.querySelector(`[data-object-id="${action.object_id}"]`);
|
||
if (targetObjEl) applyObjectProperty(targetObjEl, action.type, action.valeur);
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
}
|
||
|
||
// Effet sonore ponctuel, non bouclé — même comportement que l'action
|
||
// "Jouer un son" du Flow (static/game/js/play/actions.js), instantané : `then`
|
||
// s'exécute tout de suite, sans attendre la fin du son.
|
||
function runSonCollisionAction(action, targetEl, obj) {
|
||
if (action.asset_url) new Audio(action.asset_url).play().catch(() => {});
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
}
|
||
|
||
// Bloque la suite de la chaîne "then" jusqu'à la fin de la vidéo (ou
|
||
// "Passer") — voir static/game/js/play/video-overlay.js, même esprit qu'un
|
||
// dialogue ci-dessus.
|
||
function runVideoCollisionAction(action, targetEl, obj) {
|
||
forgeShowVideoOverlay(action.asset_url, action.mode, () => {
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
});
|
||
}
|
||
|
||
function runIndicationCollisionAction(action, targetEl, obj) {
|
||
forgeShowIndicationBubble(action.object_id, action.texte, action.duree_ms);
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
}
|
||
|
||
// Suspend la chaîne "then" pendant data_value SECONDES — même vocabulaire
|
||
// que l'action "attendre" du Flow (static/game/js/play/actions.js), utile ex.
|
||
// pour laisser un temps de lecture après une narration à l'affichage de
|
||
// l'écran.
|
||
function runAttendreCollisionAction(action, targetEl, obj) {
|
||
const seconds = Number.parseFloat(action.data_value);
|
||
const ms = Number.isNaN(seconds) ? 0 : Math.max(0, seconds) * 1000;
|
||
setTimeout(() => {
|
||
if (action.then) forgeRunCollisionRuleAction(action.then, targetEl, obj);
|
||
}, ms);
|
||
}
|
||
|
||
const COLLISION_RULE_ACTION_HANDLERS = {
|
||
dialogue: runDialogueCollisionAction,
|
||
variable: runVariableCollisionAction,
|
||
condition: runConditionCollisionAction,
|
||
surbrillance: runSurbrillanceOuVisibiliteCollisionAction,
|
||
visibilite: runSurbrillanceOuVisibiliteCollisionAction,
|
||
son: runSonCollisionAction,
|
||
video: runVideoCollisionAction,
|
||
indication: runIndicationCollisionAction,
|
||
attendre: runAttendreCollisionAction,
|
||
};
|
||
|
||
// Exécute une action (top-level, sub_action d'un "interagir", ou branche
|
||
// si_vrai/si_faux d'une "condition") — jamais appelée pour un "interagir"
|
||
// lui-même à ce niveau, voir forgeCollisionRuleTick qui le gère à part
|
||
// (affichage de bulle + attente de touche).
|
||
function forgeRunCollisionRuleAction(action, targetEl, obj) {
|
||
const handler = COLLISION_RULE_ACTION_HANDLERS[action.type];
|
||
if (handler) handler(action, targetEl, obj);
|
||
}
|
||
|
||
function forgeCollisionRuleTick(screenData) {
|
||
const player = (screenData.elements || []).find((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
|
||
const playerEl = player && document.querySelector(`[data-object-id="${player.id}"]`);
|
||
if (!player || !playerEl) return;
|
||
const playerRect = forgeElementBoxRect(playerEl, player.collision);
|
||
|
||
(screenData.elements || []).forEach((obj) => {
|
||
const rules = obj.collision_rules;
|
||
if (!rules?.length) return;
|
||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||
if (!targetEl) return;
|
||
const objRect = forgeElementBoxRect(targetEl, obj.collision);
|
||
|
||
let anyInteractActive = false;
|
||
rules.forEach((rule, ruleIndex) => {
|
||
// "clic"/"survol" sont liés à part (voir forgeBindClicSurvolRules,
|
||
// écouteurs DOM directs) — jamais évalués ici, sans quoi ils
|
||
// s'exécuteraient EN PLUS, à la proximité du joueur, en plus de
|
||
// leur propre déclencheur.
|
||
if (rule.trigger !== 'collision') return;
|
||
const stateKey = forgeCollisionRuleStateKey(obj.id, ruleIndex);
|
||
const isActive = forgeCollisionRuleTriggerActive(rule, objRect, playerRect);
|
||
const wasActive = !!forgeCollisionRuleActiveState[stateKey];
|
||
|
||
if (rule.action.type === 'interagir') {
|
||
if (isActive) {
|
||
anyInteractActive = true;
|
||
forgeShowCollisionInteractBubble(targetEl, obj.id, objRect, player.personnage_commandes?.interagir || 'E');
|
||
}
|
||
forgeCollisionRuleActiveState[stateKey] = isActive;
|
||
return;
|
||
}
|
||
|
||
if (isActive && !wasActive) forgeRunCollisionRuleAction(rule.action, targetEl, obj);
|
||
forgeCollisionRuleActiveState[stateKey] = isActive;
|
||
});
|
||
if (!anyInteractActive) forgeHideCollisionInteractBubble(obj.id, 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) {
|
||
const player = (screenData.elements || []).find((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
|
||
if (!player?.personnage_commandes || ev.key !== player.personnage_commandes.interagir) return;
|
||
(screenData.elements || []).forEach((obj) => {
|
||
const rules = obj.collision_rules;
|
||
if (!rules?.length) return;
|
||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||
if (!targetEl) return;
|
||
rules.forEach((rule, ruleIndex) => {
|
||
if (rule.action.type !== 'interagir') return;
|
||
const stateKey = forgeCollisionRuleStateKey(obj.id, ruleIndex);
|
||
if (!forgeCollisionRuleActiveState[stateKey]) return;
|
||
forgeRunCollisionRuleAction(rule.action.sub_action, targetEl, obj);
|
||
});
|
||
});
|
||
};
|
||
window.addEventListener('keydown', activeCollisionRuleInteractListener);
|
||
}
|
||
|
||
// ---------- Déclencheurs "clic"/"survol" — écouteurs DOM directs ----------
|
||
// Contrairement à "collision" (tick + proximité du joueur), ces deux
|
||
// déclencheurs n'ont besoin NI d'un joueur NI d'un tick : un écouteur
|
||
// posé une fois par objet suffit (voir game_engine/rendering/
|
||
// collision_rules.py). Re-attachés à chaque affichage d'écran, comme le
|
||
// reste de ce fichier.
|
||
let activeClicSurvolListeners = [];
|
||
|
||
function forgeUnbindClicSurvolRules() {
|
||
activeClicSurvolListeners.forEach((l) => { l.el.removeEventListener(l.event, l.handler); });
|
||
activeClicSurvolListeners = [];
|
||
}
|
||
|
||
function forgeBindClicSurvolRules(screenData) {
|
||
forgeUnbindClicSurvolRules();
|
||
(screenData.elements || []).forEach((obj) => {
|
||
const rules = obj.collision_rules;
|
||
if (!rules?.length) return;
|
||
const targetEl = document.querySelector(`[data-object-id="${obj.id}"]`);
|
||
if (!targetEl) return;
|
||
rules.forEach((rule) => {
|
||
if (rule.trigger !== 'clic' && rule.trigger !== 'survol') return;
|
||
const eventName = rule.trigger === 'clic' ? 'click' : 'mouseenter';
|
||
const handler = function () { forgeRunCollisionRuleAction(rule.action, targetEl, obj); };
|
||
targetEl.addEventListener(eventName, handler);
|
||
activeClicSurvolListeners.push({ el: targetEl, event: eventName, handler });
|
||
});
|
||
});
|
||
}
|
||
|
||
function forgeStopCollisionRuleControllers() {
|
||
if (activeCollisionRuleInterval) {
|
||
clearInterval(activeCollisionRuleInterval);
|
||
activeCollisionRuleInterval = null;
|
||
}
|
||
if (activeCollisionRuleInteractListener) {
|
||
window.removeEventListener('keydown', activeCollisionRuleInteractListener);
|
||
activeCollisionRuleInteractListener = null;
|
||
}
|
||
forgeUnbindClicSurvolRules();
|
||
forgeCollisionRuleActiveState = {};
|
||
}
|
||
|
||
// Même patron par écran que forgeStartPersonnageControllers — appelé
|
||
// juste après lui (voir static/game/js/play/screens.js). "clic"/"survol" sont
|
||
// liés inconditionnellement (aucun joueur requis) ; le tick de collision
|
||
// + l'écouteur "interagir" ne démarrent, eux, que si l'écran a un joueur
|
||
// ET au moins un objet avec des règles.
|
||
function forgeStartCollisionRuleControllers(screenData) {
|
||
forgeStopCollisionRuleControllers();
|
||
if (screenData?.kind !== 'jeu_2d') return;
|
||
const elements = screenData.elements || [];
|
||
forgeBindClicSurvolRules(screenData);
|
||
const hasPlayer = elements.some((o) => o.kind === 'personnage' && o.personnage_role === 'joueur');
|
||
const hasRules = elements.some((o) => o.collision_rules?.length);
|
||
if (!hasPlayer || !hasRules) return;
|
||
activeCollisionRuleInterval = setInterval(() => { forgeCollisionRuleTick(screenData); }, 50);
|
||
forgeStartCollisionRuleInteractListener(screenData);
|
||
}
|
||
|
||
// static/game/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,
|
||
forgeBindClicSurvolRules,
|
||
forgeUnbindClicSurvolRules,
|
||
forgeShowIndicationBubble,
|
||
};
|
||
const conditionsMod = require('./conditions.js');
|
||
global.forgeCollisionRectFromBox = global.forgeCollisionRectFromBox || conditionsMod.forgeCollisionRectFromBox;
|
||
global.forgeShapesOverlap = global.forgeShapesOverlap || conditionsMod.forgeShapesOverlap;
|
||
global.evaluateConditionClause = global.evaluateConditionClause || conditionsMod.evaluateConditionClause;
|
||
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;
|
||
const actionsMod = require('./actions.js');
|
||
global.applyObjectProperty = global.applyObjectProperty || actionsMod.applyObjectProperty;
|
||
const videoOverlayMod = require('./video-overlay.js');
|
||
global.forgeShowVideoOverlay = global.forgeShowVideoOverlay || videoOverlayMod.forgeShowVideoOverlay;
|
||
}
|