Files
Forge-Engine/static/game/js/play/collision-rules-controller.js
T
williamandClaude Sonnet 5 b2e933f322
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
Reorganisation game/document : renommage screens->game_engine + sous-dossiers game/ dans routes, scripts, static, templates, tests
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>
2026-09-19 12:27:53 +02:00

418 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ---------- É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;
}