Documents et Extensions
Les Documents sont les entités fondamentales de FoundryVTT. Apprenez à les étendre pour ajouter des comportements personnalisés à vos acteurs, objets et effets.
Introduction aux Documents Foundry
Les Documents représentent les données persistantes du monde : acteurs, objets, scènes, messages de chat, etc. Chaque Document est une instance de classe JavaScript qui encapsule des données et des comportements.
Étendre les classes de Documents permet d'ajouter :
- Des méthodes métier personnalisées - jets de dés, calculs de dégâts, repos
- Une logique de préparation des données - modificateurs, bonus calculés
- Des hooks de cycle de vie - actions lors de création, modification, suppression
- Des comportements spécifiques au système - règles de jeu, automatisations
Les DataModels définissent la structure des données (system).
Les Documents définissent le comportement et les méthodes qui manipulent ces données.
Hiérarchie des classes Document
foundry.abstract.Document
│
├── Actor
│ └── Actor5e (extension système)
│
├── Item
│ └── Item5e
│
├── ActiveEffect
│ └── ActiveEffect5e
│
├── ChatMessage
│ └── ChatMessage5e
│
├── Combat
│ └── Combat5e
│
├── Combatant
│ └── Combatant5e
│
├── JournalEntry / JournalEntryPage
├── Scene / Token
├── User
└── ... (autres documents core)
Documents principaux à étendre
| Document | Usage | Extension courante |
|---|---|---|
Actor |
Personnages, PNJ, créatures | Calculs de stats, jets, repos |
Item |
Objets, sorts, capacités | Utilisation, dégâts, propriétés |
ActiveEffect |
Buffs, debuffs, conditions | Transfert, expiration, suppression |
ChatMessage |
Messages de chat | Formatage, boutons d'action |
Combat |
Rencontres | Initiative, tours personnalisés |
Combatant |
Participants au combat | Statut, initiatives spéciales |
TokenDocument |
Représentation sur canvas | Bars, overlays, vision |
Cycle de vie d'un Document
Chaque Document passe par plusieurs phases. Comprendre ce cycle est essentiel pour savoir où placer votre logique personnalisée.
┌─────────────────────────────────────────────────────────────────┐
│ CRÉATION (_preCreate → _onCreate) │
├─────────────────────────────────────────────────────────────────┤
│ _preCreate(data, options, user) │
│ ├── Valider/modifier les données AVANT création │
│ ├── Peut annuler la création en retournant false │
│ └── Appelé côté client ET serveur │
│ │
│ _onCreate(data, options, userId) │
│ ├── Réagir APRÈS création (effets secondaires) │
│ └── Données déjà persistées en base │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ LECTURE (prepareData) │
├─────────────────────────────────────────────────────────────────┤
│ prepareData() │
│ ├── prepareBaseData() // Données de base │
│ ├── prepareEmbeddedDocuments() // Items, Effects │
│ ├── applyActiveEffects() // Appliquer les effets │
│ └── prepareDerivedData() // Données calculées │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ MODIFICATION (_preUpdate → _onUpdate) │
├─────────────────────────────────────────────────────────────────┤
│ _preUpdate(changed, options, user) │
│ ├── Modifier/valider les changements │
│ └── Peut annuler en retournant false │
│ │
│ _onUpdate(changed, options, userId) │
│ ├── Réagir aux changements │
│ └── Re-render les sheets si nécessaire │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SUPPRESSION (_preDelete → _onDelete) │
├─────────────────────────────────────────────────────────────────┤
│ _preDelete(options, user) │
│ └── Peut annuler en retournant false │
│ │
│ _onDelete(options, userId) │
│ └── Nettoyage, effets secondaires │
└─────────────────────────────────────────────────────────────────┘
_pre*: Validation et modification des données, peut annuler l'opération_on*: Effets secondaires, ne peut plus annuler
prepareData() vs prepareDerivedData()
La méthode prepareData() est appelée chaque fois qu'un Document est chargé
ou modifié. Elle orchestre plusieurs sous-méthodes dans un ordre précis.
Ordre d'exécution
prepareData() {
// 1. Réinitialiser les caches
this._preparationWarnings = [];
// 2. Préparer les données de base (AVANT les effets)
this.prepareBaseData();
// 3. Préparer les documents imbriqués (Items, Effects)
this.prepareEmbeddedDocuments();
// 4. Appliquer les ActiveEffects
this.applyActiveEffects();
// 5. Calculer les données dérivées (APRÈS les effets)
this.prepareDerivedData();
}
Différence clé
| Méthode | Timing | Usage |
|---|---|---|
prepareBaseData() |
Avant les ActiveEffects | Valeurs de base, niveau, bonus de maîtrise brut |
prepareDerivedData() |
Après les ActiveEffects | Modificateurs finaux, totaux calculés |
// Exemple pratique
export default class ActorMonSysteme extends Actor {
prepareBaseData() {
super.prepareBaseData();
// Le bonus de maîtrise est calculé AVANT les effets
// car des effets pourraient le modifier
const level = this.system.level ?? 1;
this.system.proficiency = Math.floor((level + 7) / 4);
}
prepareDerivedData() {
super.prepareDerivedData();
// Les modificateurs finaux sont calculés APRÈS les effets
// pour inclure les bonus/malus des buffs
for (const [key, ability] of Object.entries(this.system.abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2);
ability.save = ability.mod + (ability.proficient ? this.system.proficiency : 0);
}
}
}
Si vous calculez un modificateur dans prepareBaseData(),
les ActiveEffects ne pourront pas le modifier ! Placez les calculs finaux
dans prepareDerivedData().
Hooks internes du Document
Les méthodes _pre* et _on* permettent d'intercepter
les opérations CRUD (Create, Read, Update, Delete).
_preCreate - Avant la création
async _preCreate(data, options, user) {
// TOUJOURS appeler super en premier
if (await super._preCreate(data, options, user) === false) return false;
// Exemple : Configurer le token prototype par défaut
const prototypeToken = {};
if (this.type === "character") {
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
});
}
// Appliquer une taille par défaut selon la race
const size = CONFIG.MONSYSTEME.sizes[this.system.traits?.size]?.token ?? 1;
prototypeToken.width = size;
prototypeToken.height = size;
// updateSource modifie les données AVANT la création en base
this.updateSource({ prototypeToken });
// Ne pas retourner false = création autorisée
}
_onCreate - Après la création
_onCreate(data, options, userId) {
super._onCreate(data, options, userId);
// Effets secondaires après création
if (this.isEmbedded && this.type === "class") {
// Si c'est la première classe, la définir comme classe principale
if (!this.parent.system.details?.originalClass) {
this.parent.update({
"system.details.originalClass": this.id
});
}
}
}
_preUpdate - Avant la modification
async _preUpdate(changed, options, user) {
if (await super._preUpdate(changed, options, user) === false) return false;
// Exemple : Ajuster le token si la taille change
const newSize = foundry.utils.getProperty(changed, "system.traits.size");
if (newSize && newSize !== this.system.traits?.size) {
const size = CONFIG.MONSYSTEME.sizes[newSize].token ?? 1;
changed.prototypeToken ??= {};
changed.prototypeToken.width = size;
changed.prototypeToken.height = size;
}
// Exemple : Empêcher les HP négatifs
const newHP = foundry.utils.getProperty(changed, "system.attributes.hp.value");
if (newHP !== undefined && newHP < 0) {
changed.system.attributes.hp.value = 0;
}
}
_preDelete - Avant la suppression
async _preDelete(options, user) {
if (await super._preDelete(options, user) === false) return false;
// Exemple : Empêcher la suppression d'items verrouillés
if (this.flags.monsysteme?.locked) {
ui.notifications.warn("Cet objet est verrouillé et ne peut pas être supprimé.");
return false; // Annule la suppression
}
// Exemple : Demander confirmation pour les items importants
if (this.type === "class" && this.isEmbedded) {
const confirm = await Dialog.confirm({
title: "Supprimer la classe ?",
content: `Voulez-vous vraiment supprimer la classe ${this.name} ?`
});
if (!confirm) return false;
}
}
Étendre Actor pour un système personnalisé
// module/documents/actor.mjs
/**
* Extension de la classe Actor pour notre système
* @extends {Actor}
*/
export default class ActorMonSysteme extends Actor {
/** Icône par défaut pour les nouveaux acteurs */
static DEFAULT_ICON = "systems/monsysteme/icons/actor.svg";
/* -------------------------------------------- */
/* Propriétés (Getters) */
/* -------------------------------------------- */
/**
* Raccourci vers les classes du personnage
* @type {Object<string, Item>}
*/
get classes() {
if (this.type !== "character") return {};
return Object.fromEntries(
this.itemTypes.class.map(cls => [cls.identifier, cls])
);
}
/**
* L'armure actuellement équipée
* @type {Item|null}
*/
get armor() {
return this.itemTypes.armor?.find(a => a.system.equipped) ?? null;
}
/**
* Le personnage est-il mourant ?
* @type {boolean}
*/
get isDying() {
return this.system.attributes.hp.value <= 0;
}
/* -------------------------------------------- */
/* Préparation des données */
/* -------------------------------------------- */
/** @override */
prepareData() {
// Réinitialiser les caches
this._preparationWarnings = [];
this.labels = {};
super.prepareData();
// Préparer les données finales des items après tout le reste
this.items.forEach(item => item.prepareFinalAttributes?.());
}
/** @override */
prepareBaseData() {
super.prepareBaseData();
// Calculs AVANT les ActiveEffects
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
// Calculs APRÈS les ActiveEffects
}
/* -------------------------------------------- */
/* Méthodes de jet de dés */
/* -------------------------------------------- */
/**
* Effectuer un jet de compétence
* @param {string} skillId - Identifiant de la compétence
* @param {object} options - Options du jet
* @returns {Promise<Roll|null>}
*/
async rollSkill(skillId, options = {}) {
const skill = this.system.skills?.[skillId];
if (!skill) return null;
const ability = this.system.abilities[skill.ability];
const formula = `1d20 + ${ability.mod} + ${skill.proficient ? this.system.proficiency : 0}`;
const roll = new Roll(formula, this.getRollData());
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: `Jet de ${CONFIG.MONSYSTEME.skills[skillId]}`
});
return roll;
}
/**
* Effectuer un jet de sauvegarde
* @param {string} abilityId - Identifiant de la caractéristique
* @returns {Promise<Roll|null>}
*/
async rollSavingThrow(abilityId, options = {}) {
const ability = this.system.abilities?.[abilityId];
if (!ability) return null;
const formula = `1d20 + ${ability.save}`;
const roll = new Roll(formula, this.getRollData());
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: `Jet de sauvegarde de ${CONFIG.MONSYSTEME.abilities[abilityId]}`
});
return roll;
}
/* -------------------------------------------- */
/* Gestion des points de vie */
/* -------------------------------------------- */
/**
* Appliquer des dégâts à l'acteur
* @param {number} amount - Montant de dégâts
* @param {object} options - Options (type de dégâts, etc.)
* @returns {Promise<Actor>}
*/
async applyDamage(amount, options = {}) {
const hp = this.system.attributes.hp;
if (!hp) return this;
// Appliquer d'abord aux HP temporaires
let remaining = amount;
let tempDamage = 0;
if (hp.temp > 0) {
tempDamage = Math.min(hp.temp, remaining);
remaining -= tempDamage;
}
// Mettre à jour
await this.update({
"system.attributes.hp.temp": hp.temp - tempDamage,
"system.attributes.hp.value": Math.max(0, hp.value - remaining)
});
return this;
}
/**
* Soigner l'acteur
* @param {number} amount - Montant de soins
* @returns {Promise<Actor>}
*/
async applyHealing(amount) {
const hp = this.system.attributes.hp;
if (!hp) return this;
const newHP = Math.min(hp.max, hp.value + amount);
await this.update({ "system.attributes.hp.value": newHP });
return this;
}
}
Étendre Item pour un système personnalisé
// module/documents/item.mjs
/**
* Extension de la classe Item pour notre système
* @extends {Item}
*/
export default class ItemMonSysteme extends Item {
static DEFAULT_ICON = "systems/monsysteme/icons/item.svg";
/* -------------------------------------------- */
/* Propriétés */
/* -------------------------------------------- */
/**
* L'item est-il équipable ?
* @type {boolean}
*/
get isEquippable() {
return ["weapon", "armor", "equipment"].includes(this.type);
}
/**
* L'item a-t-il des utilisations limitées ?
* @type {boolean}
*/
get hasLimitedUses() {
return (this.system.uses?.max ?? 0) > 0;
}
/**
* L'item nécessite-t-il un jet d'attaque ?
* @type {boolean}
*/
get hasAttack() {
return ["weapon", "spell"].includes(this.type) && this.system.attack?.enabled;
}
/**
* L'item inflige-t-il des dégâts ?
* @type {boolean}
*/
get hasDamage() {
return (this.system.damage?.parts?.length ?? 0) > 0;
}
/* -------------------------------------------- */
/* Préparation des données */
/* -------------------------------------------- */
/** @override */
prepareData() {
super.prepareData();
this.labels = {};
}
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
this._prepareLabels();
}
/**
* Préparer les labels d'affichage
* @private
*/
_prepareLabels() {
const labels = this.labels;
// Type d'activation
const activation = this.system.activation;
if (activation?.type) {
labels.activation = `${activation.cost ?? ""} ${CONFIG.MONSYSTEME.activationTypes[activation.type] ?? ""}`.trim();
}
// Portée
const range = this.system.range;
if (range?.value) {
labels.range = `${range.value} ${range.units ?? "m"}`;
}
// Durée
const duration = this.system.duration;
if (duration?.value) {
labels.duration = `${duration.value} ${CONFIG.MONSYSTEME.durationUnits[duration.units] ?? ""}`;
}
}
/* -------------------------------------------- */
/* Utilisation de l'item */
/* -------------------------------------------- */
/**
* Utiliser l'item (action principale)
* @param {object} options - Options d'utilisation
* @returns {Promise<ChatMessage|void>}
*/
async use(options = {}) {
// Vérifier les utilisations restantes
if (this.hasLimitedUses) {
const uses = this.system.uses;
if (uses.value <= 0) {
ui.notifications.warn(`${this.name} n'a plus d'utilisations disponibles.`);
return;
}
}
// Consommer une utilisation
if (options.consumeUsage !== false && this.hasLimitedUses) {
await this.update({
"system.uses.value": this.system.uses.value - 1
});
}
// Créer le message de chat
const content = await renderTemplate(
"systems/monsysteme/templates/chat/item-card.hbs",
{ item: this, actor: this.actor, labels: this.labels }
);
return ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
content,
"flags.monsysteme.itemId": this.id
});
}
/**
* Effectuer un jet d'attaque
* @returns {Promise<Roll>}
*/
async rollAttack(options = {}) {
if (!this.hasAttack) {
throw new Error(`${this.name} ne possède pas de jet d'attaque.`);
}
const rollData = this.getRollData();
const parts = ["1d20", "@mod"];
if (this.system.attack?.bonus) {
parts.push("@bonus");
rollData.bonus = this.system.attack.bonus;
}
const formula = parts.join(" + ");
const roll = new Roll(formula, rollData);
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `${this.name} - Jet d'attaque`
});
return roll;
}
/**
* Effectuer un jet de dégâts
* @returns {Promise<Roll>}
*/
async rollDamage(options = {}) {
if (!this.hasDamage) {
throw new Error(`${this.name} n'inflige pas de dégâts.`);
}
const parts = this.system.damage.parts.map(p => p[0]);
const formula = parts.join(" + ");
const roll = new Roll(formula, this.getRollData());
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
flavor: `${this.name} - Dégâts`
});
return roll;
}
/* -------------------------------------------- */
/* Hooks de cycle de vie */
/* -------------------------------------------- */
/** @override */
async _preCreate(data, options, user) {
if (await super._preCreate(data, options, user) === false) return false;
// Définir une icône par défaut selon le type
if (!data.img || data.img === this.constructor.DEFAULT_ICON) {
const defaultIcon = CONFIG.MONSYSTEME.defaultItemIcons?.[this.type];
if (defaultIcon) {
this.updateSource({ img: defaultIcon });
}
}
}
}
Exemple complet : Actor5e simplifié
Voici une version simplifiée inspirée du système dnd5e officiel, montrant les patterns courants.
// module/documents/actor.mjs
import SystemDocumentMixin from "./mixins/document.mjs";
/**
* Actor étendu pour D&D 5e (simplifié)
* @extends {Actor}
*/
export default class Actor5e extends SystemDocumentMixin(Actor) {
static DEFAULT_ICON = "systems/dnd5e/icons/svg/documents/actor.svg";
/* -------------------------------------------- */
/* Propriétés */
/* -------------------------------------------- */
/** @type {Object<string, Item5e>} */
get classes() {
return Object.fromEntries(
this.itemTypes.class.map(cls => [cls.identifier, cls])
);
}
/** @type {Item5e|null} */
get armor() {
return this.system.attributes?.ac?.equippedArmor ?? null;
}
/** @type {Item5e|null} */
get shield() {
return this.system.attributes?.ac?.equippedShield ?? null;
}
/** @type {number} */
get level() {
if (this.type === "character") {
return this.itemTypes.class.reduce((lvl, cls) => lvl + cls.system.levels, 0);
}
return this.system.details?.level ?? 0;
}
/* -------------------------------------------- */
/* Préparation des données */
/* -------------------------------------------- */
/** @override */
prepareData() {
this._preparationWarnings = [];
this.labels = {};
super.prepareData();
this.items.forEach(item => item.prepareFinalAttributes());
}
/** @override */
prepareBaseData() {
// Calculer le niveau et le bonus de maîtrise
this._prepareBaseAbilities();
}
/** @override */
applyActiveEffects() {
// Hook pour le DataModel
if (this.system?.prepareEmbeddedData) {
this.system.prepareEmbeddedData();
}
return super.applyActiveEffects();
}
/** @override */
prepareDerivedData() {
this._prepareDerivedAbilities();
this._prepareSkills();
this._prepareArmorClass();
}
_prepareBaseAbilities() {
const dominated = {};
for (const [id, abl] of Object.entries(this.system.abilities)) {
abl.mod = Math.floor((abl.value - 10) / 2);
}
this.system.attributes.prof = Math.floor((this.level + 7) / 4);
}
_prepareDerivedAbilities() {
const dominated = this.system.attributes.prof;
for (const [id, abl] of Object.entries(this.system.abilities)) {
abl.save = abl.mod + (abl.proficient ? dominated : 0);
abl.dc = 8 + dominated + abl.mod;
}
}
_prepareSkills() {
for (const [id, skill] of Object.entries(this.system.skills ?? {})) {
const abl = this.system.abilities[skill.ability];
skill.mod = abl?.mod ?? 0;
skill.total = skill.mod + (skill.proficient ? this.system.attributes.prof : 0);
}
}
_prepareArmorClass() {
const ac = this.system.attributes.ac;
if (!ac) return;
// CA de base : 10 + Dex
let base = 10 + (this.system.abilities.dex?.mod ?? 0);
// Armure équipée
if (this.armor) {
base = this.armor.system.armor.value;
if (this.armor.system.armor.dex !== null) {
base += Math.min(this.system.abilities.dex?.mod ?? 0, this.armor.system.armor.dex);
}
}
// Bouclier
if (this.shield) {
base += this.shield.system.armor.value;
}
ac.value = base + (ac.bonus ?? 0);
}
/* -------------------------------------------- */
/* Actions */
/* -------------------------------------------- */
/**
* Effectuer un repos court
* @returns {Promise<object>}
*/
async shortRest(options = {}) {
const hd = this.system.attributes.hd;
const hp = this.system.attributes.hp;
// Dialogue pour choisir les DV à dépenser
// ... (simplifié)
return { hp: hp.value, hd: hd.value };
}
/**
* Effectuer un repos long
* @returns {Promise<object>}
*/
async longRest(options = {}) {
const hp = this.system.attributes.hp;
const hd = this.system.attributes.hd;
const updates = {
"system.attributes.hp.value": hp.max,
"system.attributes.hd.value": Math.min(hd.max, hd.value + Math.floor(hd.max / 2))
};
// Récupérer les emplacements de sorts
// ... (simplifié)
await this.update(updates);
// Message de chat
await ChatMessage.create({
speaker: ChatMessage.getSpeaker({ actor: this }),
content: `${this.name} termine un repos long.`
});
return updates;
}
/* -------------------------------------------- */
/* Hooks de cycle de vie */
/* -------------------------------------------- */
/** @override */
async _preCreate(data, options, user) {
if (await super._preCreate(data, options, user) === false) return false;
const prototypeToken = {};
if (this.type === "character") {
Object.assign(prototypeToken, {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
});
}
const size = CONFIG.DND5E.actorSizes[this.system.traits?.size]?.token ?? 1;
prototypeToken.width = size;
prototypeToken.height = size;
this.updateSource({ prototypeToken });
}
/** @override */
async _preUpdate(changed, options, user) {
if (await super._preUpdate(changed, options, user) === false) return false;
// Ajuster le token si la taille change
const newSize = foundry.utils.getProperty(changed, "system.traits.size");
if (newSize && newSize !== this.system.traits?.size) {
const size = CONFIG.DND5E.actorSizes[newSize].token ?? 1;
changed.prototypeToken ??= {};
changed.prototypeToken.width = size;
changed.prototypeToken.height = size;
}
}
}
Pattern Mixin pour partager du code
Un mixin permet de partager du code entre Actor et Item sans duplication.
// module/documents/mixins/document.mjs
/**
* Mixin ajoutant des fonctionnalités communes aux documents système
* @param {typeof Document} Base - Classe de base
* @returns {typeof Document}
*/
export default function SystemDocumentMixin(Base) {
return class SystemDocument extends Base {
/**
* Données de roll pour les formules de dés
* @returns {object}
*/
getRollData() {
const data = { ...super.getRollData() };
// Ajouter les données du DataModel
if (this.system?.getRollData) {
Object.assign(data, this.system.getRollData());
}
return data;
}
/**
* Vérifier si une propriété est modifiée par des effets
* @param {string} key - Chemin de la propriété
* @returns {boolean}
*/
isPropertyDerived(key) {
return this.overrides?.[key] !== undefined;
}
/**
* Obtenir les sources d'une modification
* @param {string} key - Chemin de la propriété
* @returns {ActiveEffect[]}
*/
getPropertyAttribution(key) {
return this.effects.filter(e =>
e.changes.some(c => c.key === key)
);
}
/**
* Afficher une notification de warning
* @param {string} message
*/
_displayWarning(message) {
this._preparationWarnings ??= [];
this._preparationWarnings.push(message);
console.warn(`${this.name}: ${message}`);
}
};
}
Utilisation du mixin
// Actor avec mixin
export default class Actor5e extends SystemDocumentMixin(Actor) {
// ...
}
// Item avec mixin
export default class Item5e extends SystemDocumentMixin(Item) {
// ...
}
Enregistrement dans CONFIG
Pour que Foundry utilise vos classes personnalisées, vous devez les enregistrer
dans CONFIG lors du hook init.
// main.mjs
import Actor5e from "./module/documents/actor.mjs";
import Item5e from "./module/documents/item.mjs";
import ActiveEffect5e from "./module/documents/active-effect.mjs";
import ChatMessage5e from "./module/documents/chat-message.mjs";
import Combat5e from "./module/documents/combat.mjs";
import Combatant5e from "./module/documents/combatant.mjs";
Hooks.once("init", function() {
console.log("Mon Système | Initialisation...");
// ═══════════════════════════════════════════════════
// ENREGISTRER LES CLASSES DE DOCUMENTS
// ═══════════════════════════════════════════════════
CONFIG.Actor.documentClass = Actor5e;
CONFIG.Item.documentClass = Item5e;
CONFIG.ActiveEffect.documentClass = ActiveEffect5e;
CONFIG.ChatMessage.documentClass = ChatMessage5e;
CONFIG.Combat.documentClass = Combat5e;
CONFIG.Combatant.documentClass = Combatant5e;
// Optionnel : Token personnalisé
// CONFIG.Token.documentClass = TokenDocument5e;
// CONFIG.Token.objectClass = Token5e;
console.log("Mon Système | Documents enregistrés !");
});
L'enregistrement doit se faire dans le hook init,
avant que le monde ne soit chargé. Si vous le faites plus tard,
les documents existants ne bénéficieront pas de vos extensions.
Pour vérifier que votre classe est bien enregistrée, ouvrez la console
et tapez CONFIG.Actor.documentClass. Vous devriez voir votre classe.
🎯 Exercice : Créer une extension d'Actor basique
Créez une classe ActorSimple qui :
- Calcule automatiquement le modificateur de chaque caractéristique dans
prepareDerivedData() - Ajoute une méthode
rollAbility(abilityId)pour effectuer un jet de caractéristique - Configure automatiquement le token prototype en
_preCreate():- Vision activée pour les "character"
- Lien d'acteur activé pour les "character"
- Empêche la suppression si l'acteur a le flag
protecteddans_preDelete()
Voir la solution
// module/documents/actor.mjs
export default class ActorSimple extends Actor {
/* -------------------------------------------- */
/* Préparation des données */
/* -------------------------------------------- */
/** @override */
prepareDerivedData() {
super.prepareDerivedData();
// Calculer les modificateurs de caractéristiques
const abilities = this.system.abilities ?? {};
for (const [key, ability] of Object.entries(abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2);
}
}
/* -------------------------------------------- */
/* Actions */
/* -------------------------------------------- */
/**
* Effectuer un jet de caractéristique
* @param {string} abilityId - ID de la caractéristique (str, dex, etc.)
* @returns {Promise<Roll|null>}
*/
async rollAbility(abilityId) {
const ability = this.system.abilities?.[abilityId];
if (!ability) {
ui.notifications.warn(`Caractéristique inconnue : ${abilityId}`);
return null;
}
const formula = `1d20 + ${ability.mod}`;
const roll = new Roll(formula);
await roll.evaluate();
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: `Jet de ${abilityId.toUpperCase()}`
});
return roll;
}
/* -------------------------------------------- */
/* Hooks de cycle de vie */
/* -------------------------------------------- */
/** @override */
async _preCreate(data, options, user) {
if (await super._preCreate(data, options, user) === false) return false;
// Configurer le token prototype pour les personnages
if (this.type === "character") {
this.updateSource({
prototypeToken: {
sight: { enabled: true },
actorLink: true,
disposition: CONST.TOKEN_DISPOSITIONS.FRIENDLY
}
});
}
}
/** @override */
async _preDelete(options, user) {
if (await super._preDelete(options, user) === false) return false;
// Empêcher la suppression des acteurs protégés
if (this.getFlag("monsysteme", "protected")) {
ui.notifications.error(`${this.name} est protégé et ne peut pas être supprimé.`);
return false;
}
}
}
// N'oubliez pas d'enregistrer dans main.mjs :
// CONFIG.Actor.documentClass = ActorSimple;
Ressources et documentation
- Document API - Classe de base de tous les Documents
- Actor API - Documentation de la classe Actor
- Item API - Documentation de la classe Item
- ActiveEffect API - Documentation des effets actifs
- Combat API - Documentation du système de combat
- Code source dnd5e - Référence pour un système complet