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.
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
});
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 :
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`);
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
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.`
);
}
});
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.