Chapitre 06

Systeme de des

Maitrisez la classe Roll de FoundryVTT pour creer des jets de des personnalises. Decouvrez D20Roll, DamageRoll et l'envoi de resultats dans le chat.

Intermediaire ~30 min de lecture

Introduction

Le systeme de des est au coeur de tout JDR. FoundryVTT fournit une classe Roll puissante et extensible pour parser des formules, lancer les des et afficher les resultats.

🎲
Architecture du systeme de des
Roll (Foundry Core)
    |
    +-- BasicRoll (Systeme)
    |       |
    |       +-- D20Roll (Jets de d20)
    |       |
    |       +-- DamageRoll (Jets de degats)
    |
    +-- ChatMessage (Affichage)

La classe Roll

La classe Roll est la base de tout jet de des dans FoundryVTT.

Creation d'un jet simple

// Jet de base
const roll = new Roll("1d20 + 5");

// Avec des donnees dynamiques
const roll = new Roll("1d20 + @abilities.str.mod", {
  abilities: { str: { mod: 3 } }
});

// Avec des options
const roll = new Roll("1d20 + @mod", data, {
  flavor: "Jet d'attaque",
  rollMode: "gmroll"
});

Proprietes importantes

const roll = new Roll("2d6 + 3");
await roll.evaluate();

// Resultat total
console.log(roll.total);    // ex: 11

// Formule originale
console.log(roll.formula);  // "2d6 + 3"

// Resultat detaille
console.log(roll.result);   // ex: "5 + 3 + 3"

// Termes du jet (des, operateurs, nombres)
console.log(roll.terms);    // [Die, OperatorTerm, NumericTerm]

// Uniquement les des
console.log(roll.dice);     // [Die(2d6)]

// Le jet a-t-il ete evalue ?
console.log(roll._evaluated); // true

Formules et modificateurs

Syntaxe de base

Formule Description
1d20Lance 1 de a 20 faces
2d6Lance 2 des a 6 faces
d20Raccourci pour 1d20
4d6 + 24d6 plus un modificateur fixe
1d20 + 1d4Combinaison de des
(2d6 + 3) * 2Operations mathematiques

Modificateurs de des

Modificateur Description Exemple
kh / kh1 Keep Highest (garder le plus haut) 2d20kh1 - Avantage
kl / kl1 Keep Lowest (garder le plus bas) 2d20kl1 - Desavantage
dh / dh1 Drop Highest (retirer le plus haut) 4d6dh1
dl / dl1 Drop Lowest (retirer le plus bas) 4d6dl1 - Stats D&D
r<2 Reroll (relancer si condition) 1d20r<2
ro=1 Reroll Once (relancer une fois) 1d20ro=1 - Halfelin
x>=6 Exploding (exploser si condition) 1d6x>=6
min3 Minimum par de 1d20min10 - Reliable Talent
cs>=8 Count Successes 10d10cs>=8
// Exemples pratiques
const avantage = new Roll("2d20kh1 + @mod", { mod: 5 });
const desavantage = new Roll("2d20kl1 + @mod", { mod: 5 });
const stats = new Roll("4d6dl1");  // Generation de caracteristique
const halflingLucky = new Roll("1d20ro=1 + @mod", { mod: 3 });
const explosion = new Roll("4d6x>=6");  // Des qui explosent

Variables (@data)

Les formules peuvent referencer des variables via la syntaxe @nomVariable.

// Donnees de l'acteur
const rollData = {
  abilities: {
    str: { value: 16, mod: 3 },
    dex: { value: 14, mod: 2 }
  },
  prof: 3,
  level: 5
};

// Reference simple
const roll1 = new Roll("1d20 + @prof", rollData);
// Formule: "1d20 + 3"

// Reference imbriquee
const roll2 = new Roll("1d20 + @abilities.str.mod", rollData);
// Formule: "1d20 + 3"

// Calcul avec variable
const roll3 = new Roll("@level * d6", rollData);
// Formule: "5d6"

// Valeur par defaut si undefined
const roll4 = new Roll("1d20 + (@bonus || 0)", { bonus: undefined });
// Formule: "1d20 + 0"

getRollData() de l'acteur

La methode getRollData() fournit toutes les donnees utiles pour les jets :

// Dans la classe Actor
getRollData() {
  const data = { ...super.getRollData() };
  
  // Ajouter les modificateurs de caracteristiques
  for (const [key, ability] of Object.entries(this.system.abilities)) {
    data.abilities[key] = {
      ...ability,
      mod: Math.floor((ability.value - 10) / 2)
    };
  }
  
  // Bonus de maitrise
  data.prof = this.system.attributes.prof;
  
  // Niveau
  data.level = this.system.details.level;
  
  return data;
}

// Utilisation
const actor = game.actors.getName("Mon Personnage");
const roll = new Roll("1d20 + @abilities.str.mod + @prof", actor.getRollData());

Evaluation des jets

Evaluation asynchrone (recommandee)

const roll = new Roll("2d6 + 3");

// Evaluation asynchrone
await roll.evaluate();

console.log(roll.total);  // Resultat disponible

Evaluation synchrone (v13+)

const roll = new Roll("1d20 + 5");

// Evaluation synchrone (moins recommandee)
roll.evaluateSync();

console.log(roll.total);

Options d'evaluation

// Maximiser tous les des
const maxRoll = new Roll("4d6");
await maxRoll.evaluate({ maximize: true });
console.log(maxRoll.total);  // 24 (6+6+6+6)

// Minimiser tous les des
const minRoll = new Roll("4d6");
await minRoll.evaluate({ minimize: true });
console.log(minRoll.total);  // 4 (1+1+1+1)

// Valider une formule sans evaluer
const isValid = Roll.validate("2d6 + @mod");
console.log(isValid);  // true ou false

Acces aux resultats detailles

const roll = new Roll("2d6 + 1d8 + 5");
await roll.evaluate();

// Parcourir les termes
for (const term of roll.terms) {
  if (term instanceof Die) {
    console.log(`${term.number}d${term.faces}:`);
    for (const result of term.results) {
      console.log(`  - ${result.result} (active: ${result.active})`);
    }
  }
}

// Acces direct aux des
const d6 = roll.dice[0];  // Premier groupe de des (2d6)
console.log(d6.total);    // Somme des 2d6
console.log(d6.results);  // [{result: 4, active: true}, {result: 2, active: true}]

Envoi vers le Chat

Methode toMessage()

const roll = new Roll("1d20 + 5");
await roll.evaluate();

// Envoi simple
await roll.toMessage();

// Avec options
await roll.toMessage({
  speaker: ChatMessage.getSpeaker({ actor: myActor }),
  flavor: "Jet de Dexterite (Acrobaties)",
  rollMode: game.settings.get("core", "rollMode")
});

// Avec flags personnalises
await roll.toMessage({
  speaker: ChatMessage.getSpeaker({ actor }),
  flavor: "Jet d'attaque - Epee longue",
  flags: {
    "mon-systeme": {
      type: "attack",
      weaponId: weapon.id,
      actorId: actor.id
    }
  }
});

Roll modes

// Jet public (visible par tous)
await roll.toMessage({ rollMode: "publicroll" });

// Jet prive MJ (visible par le joueur et le MJ)
await roll.toMessage({ rollMode: "gmroll" });

// Jet aveugle (visible uniquement par le MJ)
await roll.toMessage({ rollMode: "blindroll" });

// Jet personnel (visible uniquement par le joueur)
await roll.toMessage({ rollMode: "selfroll" });

// Utiliser le mode par defaut de l'utilisateur
const rollMode = game.settings.get("core", "rollMode");
await roll.toMessage({ rollMode });

Creer le message sans l'envoyer

// Obtenir les donnees du message sans le creer
const messageData = await roll.toMessage({
  speaker: ChatMessage.getSpeaker({ actor }),
  flavor: "Mon jet"
}, { create: false });

// Modifier les donnees
messageData.content = `
${messageData.content}
`; // Creer le message manuellement await ChatMessage.create(messageData);

Rolls personnalises

Vous pouvez etendre la classe Roll pour creer des comportements specifiques a votre systeme.

// module/dice/basic-roll.mjs
export default class BasicRoll extends Roll {
  
  /**
   * Propriete de succes basee sur une cible
   */
  get isSuccess() {
    if (!this._evaluated) return undefined;
    if (!Number.isNumeric(this.options.target)) return undefined;
    return this.total >= this.options.target;
  }
  
  /**
   * Propriete d'echec
   */
  get isFailure() {
    if (!this._evaluated) return undefined;
    if (!Number.isNumeric(this.options.target)) return undefined;
    return this.total < this.options.target;
  }
  
  /**
   * Workflow complet : configuration -> evaluation -> message
   */
  static async build(config = {}, dialog = {}, message = {}) {
    // 1. Creer le roll depuis la configuration
    const formula = (config.parts ?? []).join(" + ");
    const roll = new this(formula, config.data, config.options);
    
    // 2. Afficher un dialogue si necessaire
    if (dialog.configure) {
      const configured = await this.showDialog(roll, dialog);
      if (!configured) return null;  // Annule
    }
    
    // 3. Evaluer le jet
    await roll.evaluate();
    
    // 4. Envoyer au chat
    if (message.create !== false) {
      await roll.toMessage(message.data);
    }
    
    return roll;
  }
}

// Enregistrement dans CONFIG
Hooks.once("init", () => {
  CONFIG.Dice.BasicRoll = BasicRoll;
});

D20Roll : avantage et critiques

La classe D20Roll est specialisee pour les jets de d20 avec gestion de l'avantage/desavantage et des critiques.

// module/dice/d20-roll.mjs
export default class D20Roll extends BasicRoll {
  
  // Modes d'avantage
  static ADV_MODE = {
    NORMAL: 0,
    ADVANTAGE: 1,
    DISADVANTAGE: -1
  };
  
  constructor(formula, data, options = {}) {
    super(formula, data, options);
    
    // Configurer le mode d'avantage
    if (!this.options.configured) {
      this.configureModifiers();
    }
  }
  
  /**
   * Reference au de d20 principal
   */
  get d20() {
    return this.dice[0];
  }
  
  /**
   * Est-ce un critique ?
   */
  get isCritical() {
    if (!this._evaluated) return undefined;
    const threshold = this.options.criticalSuccess ?? 20;
    return this.d20.total >= threshold;
  }
  
  /**
   * Est-ce un echec critique ?
   */
  get isFumble() {
    if (!this._evaluated) return undefined;
    const threshold = this.options.criticalFailure ?? 1;
    return this.d20.total <= threshold;
  }
  
  /**
   * Configure les modificateurs selon les options
   */
  configureModifiers() {
    const { advantage, disadvantage } = this.options;
    
    // Determiner le mode
    if (advantage && !disadvantage) {
      this.options.advantageMode = D20Roll.ADV_MODE.ADVANTAGE;
    } else if (!advantage && disadvantage) {
      this.options.advantageMode = D20Roll.ADV_MODE.DISADVANTAGE;
    } else {
      this.options.advantageMode = D20Roll.ADV_MODE.NORMAL;
    }
    
    // Modifier la formule du d20
    const d20 = this.terms[0];
    if (d20 instanceof Die && d20.faces === 20) {
      // Supprimer les anciens modificateurs kh/kl
      d20.modifiers = d20.modifiers.filter(m => !["kh", "kl"].includes(m));
      
      switch (this.options.advantageMode) {
        case D20Roll.ADV_MODE.ADVANTAGE:
          d20.number = this.options.elvenAccuracy ? 3 : 2;
          d20.modifiers.push("kh");
          break;
        case D20Roll.ADV_MODE.DISADVANTAGE:
          d20.number = 2;
          d20.modifiers.push("kl");
          break;
        default:
          d20.number = 1;
      }
      
      // Halfelin chanceux (relancer les 1)
      if (this.options.halflingLucky) {
        d20.modifiers.push("ro=1");
      }
      
      // Reliable Talent (minimum 10)
      if (this.options.reliableTalent) {
        d20.modifiers.push("min10");
      }
    }
    
    // Reconstruire la formule
    this._formula = this.constructor.getFormula(this.terms);
    this.options.configured = true;
  }
}

// Enregistrement
Hooks.once("init", () => {
  CONFIG.Dice.D20Roll = D20Roll;
});

Utilisation de D20Roll

// Jet simple
const roll = new D20Roll("1d20 + @mod", { mod: 5 });
await roll.evaluate();
await roll.toMessage({ flavor: "Jet de sauvegarde" });

// Avec avantage
const advRoll = new D20Roll("1d20 + @mod", { mod: 5 }, {
  advantage: true
});
await advRoll.evaluate();
// Formule devient: "2d20kh + 5"

// Avec desavantage et Halfelin chanceux
const disadvRoll = new D20Roll("1d20 + @mod", { mod: 3 }, {
  disadvantage: true,
  halflingLucky: true
});
await disadvRoll.evaluate();
// Formule devient: "2d20klro=1 + 3"

// Verification du critique
if (roll.isCritical) {
  console.log("Critique !");
} else if (roll.isFumble) {
  console.log("Echec critique !");
}

DamageRoll : degats

La classe DamageRoll gere les jets de degats avec multiplicateurs critiques.

// module/dice/damage-roll.mjs
export default class DamageRoll extends BasicRoll {
  
  constructor(formula, data, options = {}) {
    super(formula, data, options);
    
    if (!this.options.configured) {
      this.configureDamage();
    }
  }
  
  /**
   * Est-ce un jet critique ?
   */
  get isCritical() {
    return this.options.isCritical === true;
  }
  
  /**
   * Type de degats (slashing, fire, etc.)
   */
  get damageType() {
    return this.options.type;
  }
  
  /**
   * Configure les degats critiques
   */
  configureDamage() {
    if (!this.isCritical) {
      this.options.configured = true;
      return;
    }
    
    const critical = this.options.critical ?? {};
    const multiplier = critical.multiplier ?? 2;
    const bonusDice = critical.bonusDice ?? 0;
    
    // Doubler les des
    for (const term of this.terms) {
      if (term instanceof Die) {
        // Sauvegarder le nombre original
        term.options.baseNumber = term.options.baseNumber ?? term.number;
        
        // Appliquer le multiplicateur
        term.number = term.options.baseNumber * multiplier;
        
        // Ajouter les des bonus
        if (bonusDice > 0) {
          term.number += bonusDice;
        }
        
        term.options.critical = true;
      }
    }
    
    // Ajouter des degats bonus (ex: "1d8" de Brutal Critical)
    if (critical.bonusDamage) {
      const bonusTerms = new Roll(critical.bonusDamage, this.data).terms;
      this.terms.push(new OperatorTerm({ operator: "+" }));
      this.terms.push(...bonusTerms);
    }
    
    // Reconstruire la formule
    this._formula = this.constructor.getFormula(this.terms);
    this.options.configured = true;
  }
}

// Enregistrement
Hooks.once("init", () => {
  CONFIG.Dice.DamageRoll = DamageRoll;
});

Utilisation de DamageRoll

// Degats normaux
const damage = new DamageRoll("2d6 + @mod", { mod: 4 }, {
  type: "slashing"
});
await damage.evaluate();
await damage.toMessage({ flavor: "Degats - Epee longue" });

// Degats critiques
const critDamage = new DamageRoll("2d6 + @mod", { mod: 4 }, {
  type: "slashing",
  isCritical: true,
  critical: {
    multiplier: 2,  // Double les des
    bonusDice: 1    // +1 de bonus (Brutal Critical)
  }
});
await critDamage.evaluate();
// Formule devient: "5d6 + 4" (2*2 + 1 des)

// Plusieurs types de degats
const fireDamage = new DamageRoll("1d6", {}, {
  type: "fire",
  properties: ["magical"]  // Proprietes pour les resistances
});

// Agregation des degats
const totalDamage = damage.total + fireDamage.total;

Exercice pratique : Creer un jet de competence

Implementons une fonction complete de jet de competence.

// module/dice/skill-roll.mjs

/**
 * Effectue un jet de competence
 * @param {Actor} actor - L'acteur qui fait le jet
 * @param {string} skillId - Identifiant de la competence
 * @param {object} options - Options du jet
 * @returns {Promise}
 */
export async function rollSkill(actor, skillId, options = {}) {
  // 1. Recuperer les donnees de la competence
  const skill = actor.system.skills[skillId];
  if (!skill) {
    ui.notifications.error(`Competence "${skillId}" introuvable.`);
    return null;
  }
  
  // 2. Recuperer les donnees de caracteristique associee
  const ability = actor.system.abilities[skill.ability];
  const rollData = actor.getRollData();
  
  // 3. Construire les parties de la formule
  const parts = ["1d20"];
  
  // Modificateur de caracteristique
  parts.push("@abilityMod");
  rollData.abilityMod = ability.mod;
  
  // Bonus de maitrise si competent
  if (skill.proficient) {
    parts.push("@prof");
    rollData.prof = actor.system.attributes.prof;
    
    // Expertise (double maitrise)
    if (skill.expertise) {
      rollData.prof *= 2;
    }
  }
  
  // Bonus divers
  if (skill.bonus) {
    parts.push("@skillBonus");
    rollData.skillBonus = skill.bonus;
  }
  
  // 4. Determiner l'avantage/desavantage
  const advantageMode = determineAdvantage(actor, skillId, options);
  
  // 5. Creer le roll
  const roll = new CONFIG.Dice.D20Roll(parts.join(" + "), rollData, {
    advantage: advantageMode === 1,
    disadvantage: advantageMode === -1,
    reliableTalent: skill.value >= 2 && actor.flags?.dnd5e?.reliableTalent,
    halflingLucky: actor.flags?.dnd5e?.halflingLucky,
    target: options.dc  // Difficulte optionnelle
  });
  
  // 6. Afficher le dialogue de configuration (optionnel)
  if (!options.fastForward) {
    const configured = await showRollDialog(roll, {
      title: `${CONFIG.MON_SYSTEME.skills[skillId].label} (${CONFIG.MON_SYSTEME.abilities[skill.ability].label})`,
      defaultRollMode: game.settings.get("core", "rollMode")
    });
    
    if (!configured) return null;  // Dialogue annule
  }
  
  // 7. Evaluer le jet
  await roll.evaluate();
  
  // 8. Envoyer au chat
  const abilityLabel = CONFIG.MON_SYSTEME.abilities[skill.ability]?.label ?? skill.ability;
  const skillLabel = CONFIG.MON_SYSTEME.skills[skillId]?.label ?? skillId;
  
  await roll.toMessage({
    speaker: ChatMessage.getSpeaker({ actor }),
    flavor: `${skillLabel} (${abilityLabel})`,
    rollMode: options.rollMode ?? game.settings.get("core", "rollMode"),
    flags: {
      "mon-systeme": {
        roll: {
          type: "skill",
          skillId: skillId,
          abilityId: skill.ability
        }
      }
    }
  });
  
  // 9. Retourner le roll pour traitement ulterieur
  return roll;
}

/**
 * Determine le mode d'avantage pour un jet
 */
function determineAdvantage(actor, skillId, options) {
  // Avantage/desavantage force
  if (options.advantage) return 1;
  if (options.disadvantage) return -1;
  
  // Verifier les effets actifs sur l'acteur
  // (implementation simplifiee)
  for (const effect of actor.effects) {
    if (effect.disabled) continue;
    
    // Logique de verification des effets...
  }
  
  return 0;  // Normal
}

/**
 * Affiche un dialogue de configuration du jet
 */
async function showRollDialog(roll, options = {}) {
  return new Promise((resolve) => {
    new Dialog({
      title: options.title ?? "Configuration du jet",
      content: `
        
`, buttons: { roll: { icon: '', label: "Lancer", callback: (html) => { const form = html.find("form")[0]; // Appliquer les options du formulaire au roll roll.options.advantage = form.advantage.checked; roll.options.disadvantage = form.disadvantage.checked; roll.configureModifiers(); resolve(true); } }, cancel: { icon: '', label: "Annuler", callback: () => resolve(false) } }, default: "roll" }).render(true); }); } // Export pour utilisation dans le systeme export { rollSkill };

Integration dans l'acteur

// Dans la classe Actor
import { rollSkill } from "../dice/skill-roll.mjs";

class MonActor extends Actor {
  /**
   * Effectue un jet de competence
   * @param {string} skillId - Identifiant de la competence
   * @param {object} options - Options du jet
   */
  async rollSkill(skillId, options = {}) {
    return rollSkill(this, skillId, options);
  }
}

// Utilisation depuis une sheet ou un script
const actor = game.actors.getName("Mon Personnage");
await actor.rollSkill("acrobatics", { fastForward: true });
Felicitations !

Vous maitrisez maintenant le systeme de des de FoundryVTT. Vous pouvez creer des jets personnalises avec avantage, critiques, et les afficher dans le chat avec style.