Chapitre 05

Système de Hooks

Maitrisez le système d'événements de FoundryVTT. Interceptez les actions du système et réagissez aux changements pour étendre les fonctionnalités.

Intermediaire ~25 min de lecture

Introduction

Les Hooks sont le systeme d'evenements de FoundryVTT. Ils permettent d'intercepter et de reagir aux actions du cycle de vie de l'application. C'est le mecanisme principal d'extension et de personnalisation.

🔗
Architecture des Hooks
Foundry Core -----> Hooks.callAll("eventName")
                              |
                              v
                    +-------------------+
                    | File d'attente    |
                    +-------------------+
                    | listener1() --OK  |
                    | listener2() --OK  |
                    | listener3() --OK  |
                    +-------------------+

Systeme/Module ---> Hooks.on("eventName", callback)

API des Hooks

Hooks.on(name, callback)

Enregistre un listener permanent qui sera appele a chaque declenchement du hook.

// Ecouter un hook de facon permanente
Hooks.on("updateActor", (actor, changes, options, userId) => {
  console.log(`${actor.name} a ete modifie`);
});

// Retourne un ID pour pouvoir desinscrire le listener
const hookId = Hooks.on("renderActorSheet", (sheet, html, data) => {
  // Modifier le DOM de la feuille
  html.querySelector(".window-header").style.background = "blue";
});

// Desinscrire le listener
Hooks.off("renderActorSheet", hookId);

Hooks.once(name, callback)

Enregistre un listener qui ne sera execute qu'une seule fois.

// Execute une seule fois au demarrage
Hooks.once("ready", () => {
  console.log("Le monde est charge !");
  // Initialisation unique
});

// Utile pour attendre un evenement specifique
Hooks.once("renderChatMessage", (message, html, data) => {
  if (message.id === targetMessageId) {
    // Reagir au rendu de CE message specifique
  }
});

Hooks.off(name, idOrCallback)

Desactive un listener enregistre.

// Avec l'ID retourne par Hooks.on()
const hookId = Hooks.on("updateActor", myCallback);
// Plus tard...
Hooks.off("updateActor", hookId);

// Avec la reference de la fonction
function myCallback(actor, changes) { /* ... */ }
Hooks.on("updateActor", myCallback);
// Plus tard...
Hooks.off("updateActor", myCallback);

call vs callAll

Foundry utilise deux methodes pour declencher les hooks :

Aspect Hooks.call() Hooks.callAll()
Blocage Peut etre bloque par return false Ne peut PAS etre bloque
Usage Evenements pre- (avant action) Evenements post- (apres action)
Retour false si bloque, sinon true Toujours true
Exemples preCreateActor, preUpdate* createActor, update*, render*
// Hook "pre" - peut bloquer l'action
Hooks.on("preCreateActor", (document, data, options, userId) => {
  // Empecher la creation de PNJ par les non-MJ
  if (!game.user.isGM && data.type === "npc") {
    ui.notifications.warn("Seul le MJ peut creer des PNJ");
    return false;  // BLOQUE la creation
  }
  // return true ou undefined = continue
});

// Hook "post" - informatif, ne peut pas bloquer
Hooks.on("createActor", (document, options, userId) => {
  console.log(`Acteur ${document.name} cree !`);
  return false;  // N'a AUCUN effet de blocage
});
⚠️
Attention aux hooks asynchrones

Foundry n'attend PAS les callbacks asynchrones. Pour des operations async avant creation/modification, utilisez les methodes _preCreate, _preUpdate du Document.

Hooks du cycle de vie

Ces hooks sont executes une seule fois au demarrage, dans cet ordre precis :

🔄
Ordre d'execution
1. init        --> Configuration du systeme
2. i18nInit    --> Traductions chargees
3. setup       --> Tous les modules initialises
4. ready       --> Monde entierement charge

1. Hook init

Premier hook, declenche AVANT le chargement des donnees du monde.

Hooks.once("init", function() {
  console.log("Mon Systeme | Initialisation");
  
  // 1. Exposer le systeme globalement
  globalThis.monSysteme = game.monSysteme = {
    config: MON_SYSTEME_CONFIG,
    documents: {},
    applications: {}
  };
  
  // 2. Enregistrer la configuration
  CONFIG.MON_SYSTEME = MON_SYSTEME_CONFIG;
  
  // 3. Enregistrer les classes de documents
  CONFIG.Actor.documentClass = MonActorClass;
  CONFIG.Item.documentClass = MonItemClass;
  
  // 4. Enregistrer les DataModels
  CONFIG.Actor.dataModels = {
    character: CharacterDataModel,
    npc: NPCDataModel
  };
  
  // 5. Enregistrer les classes de des
  CONFIG.Dice.BasicRoll = BasicRoll;
  CONFIG.Dice.D20Roll = D20Roll;
  
  // 6. Enregistrer les feuilles de personnage
  DocumentSheetConfig.registerSheet(Actor, "mon-systeme", CharacterSheet, {
    types: ["character"],
    makeDefault: true,
    label: "MON_SYSTEME.SheetClass.Character"
  });
  
  // 7. Enregistrer les parametres systeme
  registerSystemSettings();
  
  // 8. Precharger les templates Handlebars
  preloadHandlebarsTemplates();
  
  // CE QUI N'EST PAS DISPONIBLE :
  // - game.actors, game.items (pas encore charges)
  // - game.users (liste incomplete)
  // - Donnees du monde
});

2. Hook i18nInit

Declenche apres le chargement du systeme d'internationalisation.

Hooks.once("i18nInit", () => {
  // Traductions disponibles
  console.log(game.i18n.localize("MON_SYSTEME.Welcome"));
  
  // Pre-localiser la configuration
  for (const [key, value] of Object.entries(CONFIG.MON_SYSTEME.abilities)) {
    value.label = game.i18n.localize(value.label);
  }
  
  // Configurer les effets de statut avec labels localises
  CONFIG.statusEffects = CONFIG.statusEffects.map(effect => ({
    ...effect,
    label: game.i18n.localize(effect.label)
  }));
});

3. Hook setup

Declenche apres que tous les systemes et modules ont termine leur init.

Hooks.once("setup", function() {
  // Configurations qui dependent des modules
  
  // 1. Verifier la presence de modules optionnels
  if (game.modules.get("advanced-macros")?.active) {
    CONFIG.MON_SYSTEME.advancedMacrosEnabled = true;
  }
  
  // 2. Configurer les attributs trackables (pour les barres de token)
  CONFIG.Actor.trackableAttributes = {
    character: {
      bar: ["hp", "ac"],
      value: ["hp.value", "ac.value"]
    }
  };
  
  // 3. Creer du CSS dynamique
  const style = document.createElement("style");
  style.innerHTML = generateDynamicCSS();
  document.head.append(style);
  
  // PAS ENCORE DISPONIBLE :
  // - game.actors, game.items
});

4. Hook ready

Declenche quand le monde est entierement charge. C'est ici que vous avez acces a tout.

Hooks.once("ready", function() {
  console.log("Mon Systeme | Pret !");
  
  // MAINTENANT DISPONIBLE :
  // - game.actors     : Collection des acteurs
  // - game.items      : Collection des items
  // - game.scenes     : Collection des scenes
  // - game.users      : Collection des utilisateurs
  // - game.world      : Donnees du monde
  // - canvas          : Le canvas de jeu
  
  // 1. Hooks d'interaction utilisateur
  Hooks.on("hotbarDrop", (bar, data, slot) => {
    if (data.type === "Item") {
      createItemMacro(data, slot);
      return false;  // Empeche le comportement par defaut
    }
  });
  
  // 2. Afficher un message de bienvenue
  if (game.user.isGM) {
    ChatMessage.create({
      content: `

Bienvenue dans Mon Systeme !

Version ${game.system.version}

`, whisper: [game.user.id] }); } // 3. Migrations (GM uniquement) if (!game.user.isGM) return; const currentVersion = game.settings.get("mon-systeme", "migrationVersion"); const systemVersion = game.system.version; if (foundry.utils.isNewerVersion(systemVersion, currentVersion)) { performMigrations(); game.settings.set("mon-systeme", "migrationVersion", systemVersion); } });

Hooks de documents

Ces hooks sont declenches lors des operations CRUD sur les documents.

Pattern pre/post

/*
 * CYCLE CRUD D'UN DOCUMENT
 * 
 * CREATE:
 *   preCreate[Document] --> Peut bloquer avec return false
 *          |
 *          v
 *   create[Document]    --> Informatif (post-creation)
 * 
 * UPDATE:
 *   preUpdate[Document] --> Peut modifier/bloquer
 *          |
 *          v
 *   update[Document]    --> Informatif (post-modification)
 * 
 * DELETE:
 *   preDelete[Document] --> Peut bloquer la suppression
 *          |
 *          v
 *   delete[Document]    --> Informatif (post-suppression)
 */

Hooks de creation

// AVANT creation - peut bloquer
Hooks.on("preCreateActor", (document, data, options, userId) => {
  // Modifier les donnees avant creation
  document.updateSource({ 
    "system.hp.value": 10,
    "system.hp.max": 10
  });
  
  // Bloquer la creation si conditions non remplies
  if (!game.user.isGM && data.type === "npc") {
    ui.notifications.warn("Seul le MJ peut creer des PNJ");
    return false;
  }
});

// APRES creation
Hooks.on("createActor", (document, options, userId) => {
  console.log(`Acteur ${document.name} cree par ${game.users.get(userId).name}`);
  
  // Creer des items par defaut
  if (document.type === "character" && document.isOwner) {
    document.createEmbeddedDocuments("Item", [
      { name: "Equipement de base", type: "container" }
    ]);
  }
});

Hooks de modification

// AVANT modification
Hooks.on("preUpdateActor", (document, changes, options, userId) => {
  // Verifier/modifier les changements
  const newHp = foundry.utils.getProperty(changes, "system.hp.value");
  const maxHp = document.system.hp.max;
  
  // Plafonner les PV au maximum
  if (newHp !== undefined && newHp > maxHp) {
    foundry.utils.setProperty(changes, "system.hp.value", maxHp);
  }
  
  // Empecher les PV negatifs
  if (newHp !== undefined && newHp < 0) {
    foundry.utils.setProperty(changes, "system.hp.value", 0);
  }
});

// APRES modification
Hooks.on("updateActor", (document, changes, options, userId) => {
  // Reagir aux changements
  if (foundry.utils.hasProperty(changes, "system.hp.value")) {
    const hp = document.system.hp;
    
    if (hp.value === 0 && !document.system.isDead) {
      ui.notifications.info(`${document.name} est tombe inconscient !`);
    }
  }
  
  // Reagir a un changement de niveau
  if (foundry.utils.hasProperty(changes, "system.level")) {
    const newLevel = document.system.level;
    ui.notifications.info(`${document.name} atteint le niveau ${newLevel} !`);
  }
});

Hooks de suppression

// AVANT suppression
Hooks.on("preDeleteActor", (document, options, userId) => {
  // Empecher la suppression de certains acteurs
  if (document.flags?.monSysteme?.protected) {
    ui.notifications.error("Cet acteur est protege et ne peut pas etre supprime.");
    return false;
  }
  
  // Confirmation pour les PJ
  if (document.type === "character" && !options.skipConfirm) {
    // Note: Dans un vrai cas, utilisez Dialog.confirm() dans une methode async
    console.warn("Suppression d'un personnage joueur !");
  }
});

// APRES suppression
Hooks.on("deleteActor", (document, options, userId) => {
  console.log(`Acteur ${document.name} supprime`);
  
  // Nettoyer les references (ex: dans un journal)
});

Liste des documents

// Tous les documents supportent ces hooks :
// preCreate[Type], create[Type]
// preUpdate[Type], update[Type]
// preDelete[Type], delete[Type]

// Types disponibles :
// - Actor, Item, ActiveEffect
// - ChatMessage, Combat, Combatant
// - Scene, Token, Tile, Drawing, Note, Wall, Light
// - JournalEntry, JournalEntryPage
// - Macro, Playlist, PlaylistSound
// - RollTable, TableResult
// - User, Folder, Setting

Hooks de rendu

Declenches lors du rendu des Applications et fenrtes.

// Pattern: render[NomDeLApplication]
Hooks.on("renderActorSheet", (app, html, data) => {
  // app   = Instance de l'application
  // html  = Element DOM (HTMLElement dans AppV2)
  // data  = Donnees passees au template
  
  // Ajouter un bouton personnalise dans l'en-tete
  const header = html.querySelector(".window-header");
  const button = document.createElement("button");
  button.className = "custom-button";
  button.innerHTML = '';
  button.addEventListener("click", () => {
    app.document.rollInitiative();
  });
  header.querySelector(".window-title").after(button);
});

// Hooks de rendu courants
Hooks.on("renderChatLog", (app, html, data) => {
  // Modifier le chat log (ajouter des boutons, etc.)
});

Hooks.on("renderChatMessage", (message, html, data) => {
  // Modifier un message de chat apres rendu
  // Ideal pour ajouter des boutons d'action
  if (message.isRoll) {
    const damageButton = document.createElement("button");
    damageButton.textContent = "Appliquer les degats";
    damageButton.addEventListener("click", () => applyDamage(message));
    html.querySelector(".message-content").append(damageButton);
  }
});

Hooks.on("renderCombatTracker", (app, html, data) => {
  // Ajouter des controles au tracker de combat
});

Hooks.on("renderSettings", (app, html) => {
  // Ajouter des elements au menu Settings
});

Hooks.on("renderCompendiumDirectory", (app, html) => {
  // Ajouter un bouton (comme le Compendium Browser)
});

Hooks personnalises

Vous pouvez creer vos propres hooks pour permettre aux modules d'etendre votre systeme.

Convention de nommage

// Format recommande : [nomDuSysteme].[pre?][action][Objet?]

// Exemples :
"monSysteme.preRollAttack"     // Avant un jet d'attaque
"monSysteme.rollAttack"        // Apres un jet d'attaque
"monSysteme.preCastSpell"      // Avant lancer un sort
"monSysteme.castSpell"         // Apres avoir lance un sort
"monSysteme.preShortRest"      // Avant repos court
"monSysteme.shortRest"         // Apres repos court
"monSysteme.applyDamage"       // Application de degats

Declaration et declenchement

// Dans votre systeme
async function castSpell(actor, spell, options = {}) {
  // 1. Hook "pre" - peut bloquer
  const allowed = Hooks.call("monSysteme.preCastSpell", actor, spell, options);
  if (allowed === false) {
    ui.notifications.warn("Le lancement du sort a ete bloque.");
    return null;
  }
  
  // 2. Logique principale
  const result = await performSpellCast(actor, spell, options);
  
  // 3. Hook "post" - informatif
  Hooks.callAll("monSysteme.castSpell", actor, spell, result, options);
  
  return result;
}

// Utilisation par un module tiers
Hooks.on("monSysteme.preCastSpell", (actor, spell, options) => {
  // Verifier si le sort est interdit
  if (spell.system.forbidden) {
    ui.notifications.error("Ce sort est interdit !");
    return false;  // Bloque le lancement
  }
  
  // Modifier les options
  options.bonusDamage = 2;  // Ajouter des degats bonus
});

Hooks.on("monSysteme.castSpell", (actor, spell, result, options) => {
  // Logger le resultat
  console.log(`${actor.name} a lance ${spell.name} avec resultat:`, result);
  
  // Effets visuels, sons, etc.
  playSpellAnimation(spell);
});

Bonnes pratiques

// 1. TOUJOURS prefixer avec le nom du systeme
// Bon
Hooks.callAll("monSysteme.rollAttack", ...);
// Mauvais - risque de collision
Hooks.callAll("rollAttack", ...);

// 2. Documenter les parametres avec JSDoc
/**
 * Un hook declenche avant qu'un sort soit lance.
 * @function monSysteme.preCastSpell
 * @memberof hookEvents
 * @param {Actor} actor     L'acteur qui lance le sort
 * @param {Item} spell      Le sort lance
 * @param {object} options  Options de lancement
 * @param {boolean} [options.advantage] - Avantage sur le jet
 * @returns {boolean}       Retourner false pour bloquer
 */

// 3. Passer un contexte riche
Hooks.callAll("monSysteme.levelUp", actor, {
  oldLevel: 4,
  newLevel: 5,
  classItem: classItem,
  choices: selectedChoices,
  updates: pendingUpdates
});

// 4. Pattern pre/post coherent
Hooks.call("monSysteme.preAction", ...);    // Peut bloquer
Hooks.callAll("monSysteme.action", ...);    // Principal
Hooks.callAll("monSysteme.postAction", ...); // Nettoyage

Debugging des Hooks

Activer le mode debug

// Activer le debug de TOUS les hooks
CONFIG.debug.hooks = true;

// Chaque hook declence affichera dans la console :
// [DEBUG] Hook called: renderActorSheet
// [DEBUG] Hook called: updateActor

Logger un hook specifique

// Intercepter tous les hooks de votre systeme
const originalCallAll = Hooks.callAll;
Hooks.callAll = function(name, ...args) {
  if (name.startsWith("monSysteme.")) {
    console.log(`[MON_SYSTEME] Hook: ${name}`, args);
  }
  return originalCallAll.call(this, name, ...args);
};

// Logger un hook precis
Hooks.on("updateActor", (actor, changes, options, userId) => {
  console.log("updateActor:", {
    actor: actor.name,
    changes,
    options,
    user: game.users.get(userId)?.name
  });
});

Lister les listeners

// Voir tous les listeners d'un hook
console.log(Hooks.events["updateActor"]);

// Structure :
// [
//   { id: 1, fn: [Function], once: false },
//   { id: 2, fn: [Function], once: true },
//   ...
// ]

// Compter les listeners
console.log(`updateActor a ${Hooks.events["updateActor"]?.length ?? 0} listeners`);
📝
Astuce de debug

Utilisez debugger; dans vos callbacks pour mettre en pause l'execution et inspecter les variables dans les DevTools.

Exercice pratique : Systeme de repos

Implementons un systeme de repos avec des hooks pour permettre l'extension.

// module/rest.mjs

/**
 * Configuration du systeme de repos
 */
export const REST_CONFIG = {
  short: {
    duration: 1,      // 1 heure
    hpRecovery: 0,    // Pas de PV automatiques
    hitDice: true     // Peut utiliser des des de vie
  },
  long: {
    duration: 8,      // 8 heures
    hpRecovery: 1,    // 100% des PV max
    hitDice: 0.5      // Recupere 50% des des de vie
  }
};

/**
 * Effectue un repos court
 * @param {Actor} actor - L'acteur qui se repose
 * @param {object} options - Options du repos
 * @returns {Promise} Resultat du repos ou null si annule
 */
export async function shortRest(actor, options = {}) {
  const config = { ...REST_CONFIG.short, ...options };
  
  // 1. Hook pre - peut bloquer
  const allowed = Hooks.call("monSysteme.preShortRest", actor, config);
  if (allowed === false) {
    console.log("Repos court bloque par un hook");
    return null;
  }
  
  // 2. Afficher le dialogue de configuration
  if (!options.skipDialog) {
    const dialogResult = await showRestDialog(actor, "short", config);
    if (!dialogResult) return null;  // Dialogue annule
    Object.assign(config, dialogResult);
  }
  
  // 3. Calculer les recuperations
  const result = {
    type: "short",
    hpRecovered: 0,
    hitDiceUsed: [],
    resourcesRecovered: []
  };
  
  // 4. Utiliser les des de vie (si demande)
  if (config.hitDiceUsed?.length > 0) {
    for (const hdClass of config.hitDiceUsed) {
      const roll = await rollHitDie(actor, hdClass);
      if (roll) {
        result.hitDiceUsed.push({
          class: hdClass,
          roll: roll.total
        });
        result.hpRecovered += roll.total;
      }
    }
  }
  
  // 5. Recuperer les ressources a recharge courte
  const resourceUpdates = {};
  for (const item of actor.items) {
    if (item.system.recharge?.type === "short") {
      resourceUpdates[`items.${item.id}.system.uses.value`] = 
        item.system.uses.max;
      result.resourcesRecovered.push(item.name);
    }
  }
  
  // 6. Hook pendant - peut modifier le resultat
  Hooks.callAll("monSysteme.shortRest", actor, config, result);
  
  // 7. Appliquer les modifications
  const updates = {
    "system.hp.value": Math.min(
      actor.system.hp.value + result.hpRecovered,
      actor.system.hp.max
    ),
    ...resourceUpdates
  };
  
  await actor.update(updates);
  
  // 8. Hook post
  Hooks.callAll("monSysteme.restCompleted", actor, result, config);
  
  // 9. Message de chat
  await createRestMessage(actor, result);
  
  return result;
}

/**
 * Effectue un repos long
 * @param {Actor} actor - L'acteur qui se repose
 * @param {object} options - Options du repos
 */
export async function longRest(actor, options = {}) {
  const config = { ...REST_CONFIG.long, ...options };
  
  // 1. Hook pre
  const allowed = Hooks.call("monSysteme.preLongRest", actor, config);
  if (allowed === false) return null;
  
  // 2. Calculer les recuperations
  const result = {
    type: "long",
    hpRecovered: 0,
    hitDiceRecovered: 0,
    resourcesRecovered: []
  };
  
  // Recuperer tous les PV
  result.hpRecovered = actor.system.hp.max - actor.system.hp.value;
  
  // Recuperer la moitie des des de vie
  const totalHitDice = actor.system.attributes.hitDice.max;
  const currentHitDice = actor.system.attributes.hitDice.value;
  const hdToRecover = Math.max(1, Math.floor(totalHitDice * config.hitDice));
  result.hitDiceRecovered = Math.min(hdToRecover, totalHitDice - currentHitDice);
  
  // 3. Recuperer toutes les ressources
  const resourceUpdates = {};
  for (const item of actor.items) {
    if (item.system.recharge?.type === "long" || 
        item.system.recharge?.type === "short") {
      resourceUpdates[`items.${item.id}.system.uses.value`] = 
        item.system.uses.max;
      result.resourcesRecovered.push(item.name);
    }
  }
  
  // 4. Hook pendant
  Hooks.callAll("monSysteme.longRest", actor, config, result);
  
  // 5. Appliquer
  await actor.update({
    "system.hp.value": actor.system.hp.max,
    "system.attributes.hitDice.value": currentHitDice + result.hitDiceRecovered,
    ...resourceUpdates
  });
  
  // 6. Hook post
  Hooks.callAll("monSysteme.restCompleted", actor, result, config);
  
  await createRestMessage(actor, result);
  return result;
}

/**
 * Cree un message de chat pour le repos
 */
async function createRestMessage(actor, result) {
  const templateData = {
    actor,
    result,
    isLong: result.type === "long"
  };
  
  const content = await renderTemplate(
    "systems/mon-systeme/templates/chat/rest-result.hbs",
    templateData
  );
  
  await ChatMessage.create({
    speaker: ChatMessage.getSpeaker({ actor }),
    content,
    flags: {
      "mon-systeme": { type: "rest", result }
    }
  });
}

Utilisation par un module tiers

// Un module peut maintenant etendre le systeme de repos

// Bloquer le repos si conditions non remplies
Hooks.on("monSysteme.preShortRest", (actor, config) => {
  // Verifier si l'acteur est en combat
  if (game.combat?.combatants.some(c => c.actor === actor)) {
    ui.notifications.warn("Impossible de se reposer en combat !");
    return false;
  }
});

// Ajouter des effets pendant le repos
Hooks.on("monSysteme.shortRest", (actor, config, result) => {
  // Ajouter un bonus de recuperation si un soigneur est present
  const healerPresent = game.actors.some(a => 
    a.items.some(i => i.name === "Kit de soins")
  );
  
  if (healerPresent) {
    result.hpRecovered += 2;
    result.healerBonus = true;
  }
});

// Notifications apres repos
Hooks.on("monSysteme.restCompleted", (actor, result, config) => {
  if (result.hpRecovered > 0) {
    ui.notifications.info(
      `${actor.name} a recupere ${result.hpRecovered} PV.`
    );
  }
});
Felicitations !

Vous maitrisez maintenant le systeme de hooks de FoundryVTT. Vous pouvez intercepter les evenements du systeme, creer vos propres hooks et permettre l'extension de votre systeme.