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 */ 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 let formula = ""; let modFormula = null; let totemBonus = { human: 0, adapted: 0 }; // Calculer les bonus/malus par domaine de totem if (skillCategory) { totemBonus = this._calculateTotemDomainBonuses(skillCategory, actor); } // Appliquer les réussites automatiques et seuils auto 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 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 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 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 if (totemBonus.adapted !== 0) { NoD += totemBonus.adapted; } // Construction de la formule modifiée if (modFormula != null) { modFormula = modFormula + "+" + adaptedFormula; } else { modFormula = adaptedFormula; } }; // Gestion du choix de totem à garder (si les deux sont activés) 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 } 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 } } // Construction de la formule de base let baseFormula = '' + NoD + "d10"; baseFormula += (adjustedDifficulty != undefined) ? "cs>=" + adjustedDifficulty : "cs>=7"; baseFormula += `[regular_${game.user.name}]` // Construction de la formule finale if (modFormula != null) { formula = baseFormula + "+" + modFormula; } else { formula = baseFormula } // Création du jet de dés let roll = new Roll(formula, actor.getRollData()); // Stocker les métadonnées du roll pour l'affichage roll.vermineData = { totemsUsed: { ...totems }, keepTotem: keepTotem, difficulty: adjustedDifficulty, originalDifficulty: difficulty, skillCategory: skillCategory, skillLevel: skillLevel, hasSpecialty: hasSpecialty, autoSuccesses: autoSuccesses, totemBonuses: { ...totemBonus }, baseNoD: NoD, rerolls: Reroll, selfControl: self_control }; //effectuer le lancé await roll.evaluate(); //afficher le lancer 3d 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 }); 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} */ static _calculateTotemDomainBonuses(skillCategory, actor) { const bonuses = { human: 0, adapted: 0 }; 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) { return bonuses; } // Vérifier si la catégorie de compétence est dans les domaines du totem const preferredCategory = actor.system.skill_categories?.preferred; // Bonus pour le totem de l'acteur 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é const oppositeTotem = CONFIG.VERMINE.totem_opposites?.[actorTotem]; if (oppositeTotem && preferredCategory) { const oppositeConfig = CONFIG.VERMINE.totemDomains[oppositeTotem]; if (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 */ 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 if (!skillLevel) return 0; let autoSuccesses = 0; switch (skillLevel) { case 2: // Compétent if (hasSpecialty) autoSuccesses = 1; break; case 3: // Expert autoSuccesses = 1; break; case 4: // Maître autoSuccesses = 1; if (hasSpecialty) autoSuccesses += 1; break; case 5: // Légende autoSuccesses = 2; break; default: autoSuccesses = 0; } return autoSuccesses; } /** * 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 */ 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 (skillLevel === 0) return 9; // Très difficile if (skillLevel === 1) return 7; // Difficile 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 */ 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 } // 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 } ev.currentTarget.classList.add('rerolled'); // Mise en place de la relance 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; 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; // Construction de la formule de relance let formula = `1d10cs>=${difficulty}`; console.log(diceType) switch (diceType.trim()) { case 'human': formula = `(1d10cs>=${difficulty}[human_${game.user.name}])*2` break; case 'adapted': formula = `(1d10cs>=${difficulty}[adapted_${game.user.name}])*2` break; default: formula += `[regular_${game.user.name}]` break; }; // Création et évaluation du jet de dés de relance let reroll = await new Roll(formula); await reroll.evaluate(); //afficher les dés 3d 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 }) } /** * Méthode pour gérer les événements de chat * @param {HTMLElement} html - L'élément HTML contenant les événements de chat */ 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") }; } else { // Activer les relances pour chaque dé for (let die of html.find('.die')) { die.classList.add("rerollable") }; } // Ajouter un événement de clic pour les dés pouvant être relancés 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); }); // Mettre à jour l'étiquette en fonction de la valeur sélectionnée html.find("#effort-reroll").change(ev => { let label = html.find("#granted-reroll")[0] label.innerText = ev.currentTarget.value }); // Ajouter un événement de clic pour accorder une relance 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 }); }); } /** * 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 */ 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); } else { return false } } /** * 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 */ static async diplayChatRoll(roll, param) { let content = await renderTemplate("systems/vermine2047/templates/roll-message.hbs", { roll, param }) let chatData = { user: game.user._id, speaker: ChatMessage.getSpeaker(), content: content, roll: roll }; let msg = await ChatMessage.create(chatData); await msg.setFlag('world', 'roll', roll); return msg } }