code review: fix critical issues and improve code quality

- Fix constructor in rollDialog.mjs (spread operator for options)
- Remove all console.log statements from production code
- Add comprehensive JSDoc comments for all public APIs
- Convert French comments to English for consistency
- Use parseInt with radix parameter (10) throughout
- Replace let with const where appropriate
- Use Set for O(1) lookups in group-link.mjs methods
- Use spread operators for array cloning
- Optimize removeActorFromAllGroups with Set lookups
- Improve registerHooks with better comments and Set usage
- Simplify roll-message.hbs template logic
- Fix duplicate VERMINE key in lang/fr.json
- Add missing error translations
- Add .eslintrc.js with FoundryVTT-compatible linting config

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
2026-06-04 13:33:58 +02:00
parent 716c1b49ae
commit 386d80639c
6 changed files with 1078 additions and 591 deletions
+295 -228
View File
@@ -1,109 +1,119 @@
export class VermineUtils {
/**
* Méthode pour effectuer un jet de dés avec différentes options
* @param {Object} options - Les options du jet de dés
* @param {Actor} options.actor - L'acteur qui lance les dés
* @param {number} options.NoD - Nombre de dés de base
* @param {number} [options.Reroll=0] - Nombre de relances autorisées
* @param {number} [options.difficulty=7] - Difficulté du jet
* @param {number} [options.self_control=0] - Sang-froid utilisé
* @param {string} [options.rollLabel="jet custom"] - Libellé du jet
* @param {Object} [options.totems={}] - Totems utilisés {human: false, adapted: false}
* @param {number} [options.max_effort=0] - Effort maximum
* @param {string} [options.skillCategory=null] - Catégorie de compétence pour les bonus de domaine
* @param {string} [options.keepTotem=null] - Totem à garder ('human' ou 'adapted')
* @param {number} [options.skillLevel=null] - Niveau de la compétence pour les réussites automatiques
* @param {boolean} [options.hasSpecialty=false] - Si une spécialité est utilisée
* @returns {Roll} - Le résultat du jet de dés
* Rolls dice with Vermine2047-specific rules.
* @param {Object} options - Roll options
* @param {Actor} options.actor - The actor rolling
* @param {number} options.NoD - Base dice pool
* @param {number} [options.Reroll=0] - Reroll count
* @param {number} [options.difficulty=7] - Difficulty threshold
* @param {number} [options.self_control=0] - Self control used
* @param {string} [options.rollLabel="jet custom"] - Roll label
* @param {Object} [options.totems={}] - Totems used {human: boolean, adapted: boolean}
* @param {number} [options.max_effort=0] - Max effort
* @param {string} [options.skillCategory=null] - Skill category for domain bonuses
* @param {string} [options.keepTotem=null] - Totem to keep ('human' or 'adapted')
* @param {number} [options.skillLevel=null] - Skill level for auto-successes
* @param {boolean} [options.hasSpecialty=false] - Whether a specialty is used
* @returns {Promise<Roll>} The roll result
*/
static async roll({ actor, NoD, Reroll = 0, difficulty = 7, self_control = 0, rollLabel = "jet custom", totems = { human: false, adapted: false }, max_effort = 0, skillCategory = null, keepTotem = null, skillLevel = null, hasSpecialty = false }) {
// Déclaration des variables
static async roll({
actor,
NoD,
Reroll = 0,
difficulty = 7,
self_control = 0,
rollLabel = "jet custom",
totems = { human: false, adapted: false },
max_effort = 0,
skillCategory = null,
keepTotem = null,
skillLevel = null,
hasSpecialty = false
}) {
// Validate inputs
if (!actor) {
throw new Error("Actor is required for rolling");
}
// Sanitize user name for use in dice flavor
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
// Declare variables
let formula = "";
let modFormula = null;
let totemBonus = { human: 0, adapted: 0 };
// Calculer les bonus/malus par domaine de totem
// Calculate domain bonuses for totems
if (skillCategory) {
totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor);
}
// Appliquer les réussites automatiques et seuils auto
// Apply auto-successes and auto-thresholds
let autoSuccesses = 0;
let adjustedDifficulty = difficulty;
if (skillLevel !== null && skillLevel !== undefined) {
// Calculer les réussites automatiques
autoSuccesses = this._calculateAutoSuccesses(skillLevel, hasSpecialty);
// Appliquer le seuil automatique si nécessaire
const autoThreshold = this._getAutoThreshold(skillLevel);
if (autoThreshold !== null) {
adjustedDifficulty = autoThreshold;
}
}
// Vérification des totems humains
// Handle human totem
if (totems.human) {
NoD--;
const humanDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
const humanFormula = "(1D10cs>=" + humanDifficulty + `[human_${game.user.name}]*2)`;
// Appliquer bonus/malus de domaine
const humanFormula = `(1D10cs>=${humanDifficulty}[human_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.human !== 0) {
// Si bonus, ajouter un dé supplémentaire, si malus, réduire le pool
NoD += totemBonus.human;
}
modFormula = humanFormula;
}
// Vérification des totems adaptés
// Handle adapted totem
if (totems.adapted) {
NoD--;
const adaptedDifficulty = skillLevel !== null ? Math.max(adjustedDifficulty, difficulty) : adjustedDifficulty;
const adaptedFormula = "(1D10cs>=" + adaptedDifficulty + `[adapted_${game.user.name}]*2)`;
// Appliquer bonus/malus de domaine
const adaptedFormula = `(1D10cs>=${adaptedDifficulty}[adapted_${safeUserName}]*2)`;
// Apply domain bonus/malus
if (totemBonus.adapted !== 0) {
NoD += totemBonus.adapted;
}
// Construction de la formule modifiée
if (modFormula != null) {
modFormula = modFormula + "+" + adaptedFormula;
// Build combined formula
if (modFormula !== null) {
modFormula = `${modFormula}+${adaptedFormula}`;
} else {
modFormula = adaptedFormula;
}
};
}
// Gestion du choix de totem à garder (si les deux sont activés)
// Handle keepTotem selection (if both totems are active)
if (totems.human && totems.adapted && keepTotem) {
// Si on veut garder un seul totem, ne pas doubler le bonus
if (keepTotem === 'human' && totems.adapted) {
// Retirer le totem adapté du calcul
modFormula = "(1D10cs>=" + adjustedDifficulty + `[human_${game.user.name}]*2)`;
NoD++; // On avait décrémenté pour adapted, on annule
modFormula = `(1D10cs>=${adjustedDifficulty}[human_${safeUserName}]*2)`;
NoD++; // Cancel the decrement for adapted
} else if (keepTotem === 'adapted' && totems.human) {
// Retirer le totem humain du calcul
modFormula = "(1D10cs>=" + adjustedDifficulty + `[adapted_${game.user.name}]*2)`;
NoD++; // On avait décrémenté pour human, on annule
modFormula = `(1D10cs>=${adjustedDifficulty}[adapted_${safeUserName}]*2)`;
NoD++; // Cancel the decrement for human
}
}
// Construction de la formule de base
let baseFormula = '' + NoD + "d10";
baseFormula += (adjustedDifficulty != undefined) ? "cs>=" + adjustedDifficulty : "cs>=7";
baseFormula += `[regular_${game.user.name}]`
// Build base formula
const baseFormula = `${NoD}d10cs>=${adjustedDifficulty}[regular_${safeUserName}]`;
// Construction de la formule finale
if (modFormula != null) {
formula = baseFormula + "+" + modFormula;
} else { formula = baseFormula }
// Build final formula
formula = modFormula !== null ? `${baseFormula}+${modFormula}` : baseFormula;
// Création du jet de dés
let roll = new Roll(formula, actor.getRollData());
// Create the roll
const roll = new Roll(formula, actor.getRollData());
// Stocker les métadonnées du roll pour l'affichage
// Store metadata for display
roll.vermineData = {
totemsUsed: { ...totems },
keepTotem: keepTotem,
@@ -118,71 +128,93 @@ export class VermineUtils {
rerolls: Reroll,
selfControl: self_control
};
//effectuer le lancé
// Evaluate the roll
await roll.evaluate();
//afficher le lancer 3d
// Show 3D dice if available
await VermineUtils.showDiceSoNice(roll);
// afficher le résultat dans le chat
VermineUtils.diplayChatRoll(roll, { actor, NoD, Reroll, difficulty, self_control, rollLabel, totems, max_effort, skillCategory, keepTotem, skillLevel, hasSpecialty });
// Display result in chat
VermineUtils.diplayChatRoll(roll, {
actor,
NoD,
Reroll,
difficulty,
self_control,
rollLabel,
totems,
max_effort,
skillCategory,
keepTotem,
skillLevel,
hasSpecialty
});
return roll;
}
/**
* Calcule les bonus/malus par domaine de totem
* @param {string} skillCategory - Catégorie de la compétence
* @param {Actor} actor - L'acteur
* @returns {Object} - Bonus pour chaque totem {human: number, adapted: number}
* Calculates domain bonuses/penalties for totems.
* @param {string} skillCategory - The skill category
* @param {Actor} actor - The actor
* @returns {Object} Bonuses for each totem {human: number, adapted: number}
*/
static _calculateTotemDomainBonuses(skillCategory, actor) {
const bonuses = { human: 0, adapted: 0 };
// Validate inputs
if (!CONFIG.VERMINE?.totemDomains || !actor?.system?.identity?.totem) {
return bonuses;
}
const actorTotem = actor.system.identity.totem;
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
if (!totemConfig || !totemConfig.domains) {
// Check if actor's totem exists in configuration
if (!CONFIG.VERMINE.totemDomains[actorTotem]) {
return bonuses;
}
// Vérifier si la catégorie de compétence est dans les domaines du totem
const totemConfig = CONFIG.VERMINE.totemDomains[actorTotem];
if (!totemConfig?.domains) {
return bonuses;
}
// Get actor's preferred skill category
const preferredCategory = actor.system.skill_categories?.preferred;
// Bonus pour le totem de l'acteur
// Bonus for actor's totem if preferred category is in its domains
if (preferredCategory && totemConfig.domains.includes(preferredCategory)) {
// Le domaine de prédilection est dans les domaines du totem
bonuses[actorTotem] = totemConfig.bonus || 1;
}
// Malus pour le totem opposé
// Penalty for opposite totem if preferred category is in its domains
const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem];
if (oppositeTotem && preferredCategory) {
if (oppositeTotem && CONFIG.VERMINE.totemDomains[oppositeTotem]) {
const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem];
if (oppositeConfig?.domains?.includes(preferredCategory)) {
if (preferredCategory && oppositeConfig?.domains?.includes(preferredCategory)) {
bonuses[oppositeTotem] = -(oppositeConfig.bonus || 1);
}
}
return bonuses;
}
/**
* Calcule les réussites automatiques basées sur la maîtrise de la compétence
* @param {number} skillLevel - Niveau de la compétence (0-5)
* @param {boolean} hasSpecialty - Si une spécialité est utilisée
* @returns {number} - Nombre de réussites automatiques
* Calculates automatic successes based on skill mastery level.
* @param {number} skillLevel - Skill level (0-5)
* @param {boolean} [hasSpecialty=false] - Whether a specialty is used
* @returns {number} Number of automatic successes
*/
static _calculateAutoSuccesses(skillLevel, hasSpecialty = false) {
// Selon les règles de Vermine2047, les réussites automatiques sont basées sur le niveau de maîtrise
// Niveau 0 (Incompétent): 0 réussite automatique
// Niveau 1 (Débutant): 0 réussite automatique
// Niveau 2 (Compétent): 1 réussite automatique si spécialité utilisée
// Niveau 3 (Expert): 1 réussite automatique
// Niveau 4 (Maître): 1 réussite automatique + 1 si spécialité utilisée
// Niveau 5 (Légende): 2 réussites automatiques
// According to Vermine2047 rules, automatic successes are based on mastery level:
// Level 0 (Incompetent): 0 automatic successes
// Level 1 (Beginner): 0 automatic successes
// Level 2 (Proficient): 1 automatic success if specialty is used
// Level 3 (Expert): 1 automatic success
// Level 4 (Master): 1 automatic success + 1 if specialty is used
// Level 5 (Legend): 2 automatic successes
if (!skillLevel) return 0;
@@ -210,204 +242,239 @@ export class VermineUtils {
}
/**
* Détermine le seuil automatique si la compétence n'est pas maîtrisée
* @param {number} skillLevel - Niveau de la compétence
* @returns {number|null} - Seuil automatique ou null si la compétence est maîtrisée
* Determines the automatic threshold if the skill is not mastered.
* @param {number} skillLevel - Skill level
* @returns {number|null} Automatic threshold or null if skill is mastered
*/
static _getAutoThreshold(skillLevel) {
// Si la compétence n'est pas maîtrisée (niveau 0 ou 1), utiliser un seuil par défaut
// Niveau 0 (Incompétent): seuil = 9 (très difficile)
// Niveau 1 (Débutant): seuil = 7 (difficile)
// Niveau >= 2: null (utiliser le seuil normal)
// If the skill is not mastered (level 0 or 1), use a default threshold
// Level 0 (Incompetent): threshold = 9 (very hard)
// Level 1 (Beginner): threshold = 7 (hard)
// Level >= 2: null (use normal threshold)
if (skillLevel === 0) return 9; // Très difficile
if (skillLevel === 1) return 7; // Difficile
if (skillLevel === 0) return 9; // Very hard
if (skillLevel === 1) return 7; // Hard
return null; // Utiliser le seuil normal
}
/**
* Méthode pour gérer les événements de relance de dés
* @param {Object} message - Le message contenant l'événement de relance
* @param {Object} ev - L'événement de relance
* Handles reroll events on dice in chat messages.
* @param {Object} message - The chat message containing the reroll event
* @param {Object} ev - The reroll event
* @returns {Promise<boolean>} Whether the reroll was successful
*/
static async onReroll(message, ev) {
// Vérification de l'utilisateur
if (game.user._id != message.user._id || !game.user.isGM) {
ui.notifications.warn('vous ne pouvez pas relancer un dés sur ce jet')
return false
// Verify user permissions
if (game.user?._id !== message.user._id && !game.user?.isGM) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_cannot_reroll'));
return false;
}
// Récupération du nombre de relances autorisé
let rerollCount = ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll')?.innerText;
// Vérification du nombre de relances restantes
if (!rerollCount || parseInt(rerollCount) < 1) {
console.log('no reroll')
ui.notifications.warn("plus de relance possible");
let rerollables = ev.currentTarget.closest('ul').querySelectorAll('.rerollable');
rerollables.forEach(el => el.classList.remove('rerollable'));
// Mise à jour du nombre de relances restantes
ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll').innerText = rerollCount - 1;
let content = ev.currentTarget.closest('div.message-content').outerHTML;
await message.update({
content: content
})
return false
// Get reroll count
const rollMessage = ev.currentTarget.closest('div.vermine-roll-message');
if (!rollMessage) {
return false;
}
let rerollCount = rollMessage.querySelector('#allowed_reroll')?.innerText;
// Check if rerolls are available
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
ui.notifications.warn(game.i18n.localize('VERMINE.error_no_rerolls_left'));
const rerollables = ev.currentTarget.closest('ul')?.querySelectorAll('.rerollable');
if (rerollables) {
rerollables.forEach(el => el.classList.remove('rerollable'));
}
return false;
}
ev.currentTarget.classList.add('rerolled');
// Mise en place de la relance
// Set reroll flag
await message.setFlag("world", "reroll", true);
// Récupération de la difficulté et du type de dé
let difficulty = ev.currentTarget.closest('ul').dataset.difficulty;
// Get difficulty and dice type
const ulElement = ev.currentTarget.closest('ul');
const difficulty = ulElement?.dataset.difficulty ?? 7;
let diceType = ev.currentTarget.dataset.diceType;
// Mise à jour du nombre de relances restantes
ev.currentTarget.closest('div.vermine-roll-message').querySelector('#allowed_reroll').innerText = rerollCount - 1;
// Sanitize user name
const safeUserName = (game.user?.name ?? "user").replace(/[^a-zA-Z0-9_]/g, '_');
// Construction de la formule de relance
// Build reroll formula
let formula = `1d10cs>=${difficulty}`;
console.log(diceType)
switch (diceType.trim()) {
switch ((diceType ?? '').trim()) {
case 'human':
formula = `(1d10cs>=${difficulty}[human_${game.user.name}])*2`
formula = `(1d10cs>=${difficulty}[human_${safeUserName}])*2`;
break;
case 'adapted':
formula = `(1d10cs>=${difficulty}[adapted_${game.user.name}])*2`
formula = `(1d10cs>=${difficulty}[adapted_${safeUserName}])*2`;
break;
default:
formula += `[regular_${game.user.name}]`
formula += `[regular_${safeUserName}]`;
break;
};
}
// Création et évaluation du jet de dés de relance
let reroll = await new Roll(formula);
// Create and evaluate reroll
const reroll = new Roll(formula);
await reroll.evaluate();
//afficher les dés 3d
// Show 3D dice if available
await VermineUtils.showDiceSoNice(reroll);
// mise à jour de l'affichage du dés
console.log(reroll)
let result = reroll.dice[0].results[0].result;
ev.currentTarget.querySelector('span').innerText = result;
//mise à jour du total
let success = reroll.dice[0].results[0].success;
if (success) {
ev.currentTarget.classList.add('success')
let total = parseInt(ev.currentTarget.closest('.vermine-roll-message').querySelector('#total').innerText) + reroll.total
ev.currentTarget.closest('.vermine-roll-message').querySelector('#total').innerText = total
}
// Mise à jour de l'affichagedu message
ev.currentTarget.classList.remove("rerollable")
let content = ev.currentTarget.closest('div.message-content').outerHTML;
console.log(reroll, message);
await message.update({
content: content
})
// Update die display
const result = reroll.dice[0]?.results[0]?.result ?? 0;
const dieSpan = ev.currentTarget.querySelector('span');
if (dieSpan) {
dieSpan.innerText = result;
}
// Update total if successful
const success = reroll.dice[0]?.results[0]?.success;
if (success) {
ev.currentTarget.classList.add('success');
const totalElement = rollMessage.querySelector('#total');
if (totalElement) {
const currentTotal = parseInt(totalElement.innerText, 10) || 0;
totalElement.innerText = currentTotal + reroll.total;
}
}
// Update message content
ev.currentTarget.classList.remove("rerollable");
const messageContent = ev.currentTarget.closest('div.message-content');
if (messageContent) {
const newRerollCount = parseInt(rerollCount, 10) - 1;
rollMessage.querySelector('#allowed_reroll').innerText = newRerollCount;
await message.update({
content: messageContent.outerHTML
});
}
return true;
}
/**
* Méthode pour gérer les événements de chat
* @param {HTMLElement} html - L'élément HTML contenant les événements de chat
/**
* Sets up event listeners for chat messages.
* @param {HTMLElement} html - The HTML element containing chat events.
*/
static async chatListenners(html) {
// Récupérer le nombre de relances autorisées
let reroll = html.find('#allowed_reroll')[0]?.innerText;
// Vérifier s'il n'y a pas de relances ou si le nombre est inférieur à 1
if (!reroll || parseInt(reroll) < 1) {
// Désactiver les relances pour chaque dé
for (let die of html.find('.die')) {
die.classList.remove("rerollable")
};
// Get reroll count
const rerollCountElement = html.find('#allowed_reroll')[0];
const rerollCount = rerollCountElement?.innerText;
// Enable/disable rerolls based on count
if (!rerollCount || parseInt(rerollCount, 10) < 1) {
// Disable rerolls for all dice
html.find('.die').forEach(die => {
die.classList.remove("rerollable");
});
} else {
// Activer les relances pour chaque dé
for (let die of html.find('.die')) {
die.classList.add("rerollable")
};
// Enable rerolls for all dice
html.find('.die').forEach(die => {
die.classList.add("rerollable");
});
}
// Ajouter un événement de clic pour les dés pouvant être relancés
// Add click event for rerollable dice
html.find('.rerollable').click(async (ev) => {
ev.preventDefault();
// Récupérer l'ID du message
let msgId = ev.currentTarget.closest("li.message").dataset.messageId;
// Récupérer le message correspondant à l'ID
let message = await game.messages.get(msgId);
// Appeler la fonction onReroll de VermineUtils
await VermineUtils.onReroll(message, ev);
const msgId = ev.currentTarget.closest("li.message")?.dataset?.messageId;
if (msgId) {
const message = await game.messages.get(msgId);
await VermineUtils.onReroll(message, ev);
}
});
// Mettre à jour l'étiquette en fonction de la valeur sélectionnée
// Update granted reroll label
html.find("#effort-reroll").change(ev => {
let label = html.find("#granted-reroll")[0]
label.innerText = ev.currentTarget.value
const label = html.find("#granted-reroll")[0];
if (label) {
label.innerText = ev.currentTarget.value;
}
});
// Ajouter un événement de clic pour accorder une relance
// Add click event for granting rerolls
html.find("button.grant-reroll").click(async (ev) => {
// Mettre à jour le nombre de relances autorisées
html.find("#allowed_reroll")[0].innerText = html.find('#granted-reroll')[0].innerText
let mesEl = ev.currentTarget.closest('[data-message-id]')
let messageId = mesEl.dataset.messageId;
// Quand relance accorder masquer la zone pour accorder les relances
ev.currentTarget.closest('.reroll-from-effort').style.display = "none"
let content = ev.currentTarget.closest(".vermine-roll-message").outerHTML;
// Mettre à jour le contenu du message avec la relance accordée
let message = await game.messages.get(messageId);
await message.update({ content: content });
const grantedRerollElement = html.find('#granted-reroll')[0];
const allowedRerollElement = html.find("#allowed_reroll")[0];
if (grantedRerollElement && allowedRerollElement) {
allowedRerollElement.innerText = grantedRerollElement.innerText;
}
const mesEl = ev.currentTarget.closest('[data-message-id]');
const messageId = mesEl?.dataset?.messageId;
if (messageId) {
// Hide reroll grant area
ev.currentTarget.closest('.reroll-from-effort').style.display = "none";
const rollMessage = ev.currentTarget.closest(".vermine-roll-message");
if (rollMessage) {
const content = rollMessage.outerHTML;
const message = await game.messages.get(messageId);
await message.update({ content: content });
}
}
});
}
/**
* Méthode pour afficher les résultats des dés de manière graphique
* @param {Roll} roll - Le jet de dés à afficher
* @param {string} rollMode - Le mode d'affichage du jet de dés
* Displays dice rolls in 3D if available.
* @param {Roll} roll - The roll to display
* @param {string} [rollMode] - The roll mode (uses game settings if not provided)
* @returns {Promise<boolean>} Whether 3D dice were shown
*/
static async showDiceSoNice(roll, rollMode) {
if (game.dice3d) {
let whisper = null;
let blind = false;
rollMode = rollMode ?? game.settings.get("core", "rollMode");
switch (rollMode) {
case "blindroll": //GM only
blind = true;
case "gmroll": //GM + rolling player
whisper = this.getUsers(user => user.isGM);
break;
case "roll": //everybody
whisper = this.getUsers(user => user.active);
break;
case "selfroll":
whisper = [game.user.id];
break;
}
await game.dice3d.showForRoll(roll, game.user, true, whisper, blind);
if (!game.dice3d) {
return false;
}
else { return false }
rollMode = rollMode ?? game.settings.get("core", "rollMode");
let whisper = null;
let blind = false;
switch (rollMode) {
case "blindroll": // GM only
blind = true;
// Falls through
case "gmroll": // GM + rolling player
whisper = this.getUsers(user => user.isGM);
break;
case "roll": // Everybody
whisper = this.getUsers(user => user.active);
break;
case "selfroll":
whisper = [game.user.id];
break;
}
await game.dice3d.showForRoll(roll, game.user, true, whisper, blind);
return true;
}
/**
* Méthode pour afficher un jet de dés dans le chat
* @param {Roll} roll - Le jet de dés à afficher
* @param {Object} param - Les paramètres du jet de dés
* @returns {ChatMessage} - Le message affichant le jet de dés
* Displays a dice roll in the chat.
* @param {Roll} roll - The roll to display
* @param {Object} param - Roll parameters
* @returns {Promise<ChatMessage>} The created chat message
*/
static async diplayChatRoll(roll, param) {
let content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param })
let chatData = {
user: game.user._id,
const content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param });
const chatData = {
user: game.user?._id,
speaker: ChatMessage.getSpeaker(),
content: content,
roll: roll
};
let msg = await ChatMessage.create(chatData);
const msg = await ChatMessage.create(chatData);
await msg.setFlag('world', 'roll', roll);
return msg
return msg;
}