Finalisation complète du système Vermine2047 pour FoundryVTT v14
Implémentations majeures: - Classe GroupLink pour synchronisation bidirectionnelle acteurs↔groupes - Configuration complète des totems, PNJ et créatures - Redesign du RollDialog avec interface compacte et sélecteurs - Bonus/malus par domaine de totem - Réussites automatiques et seuils auto basés sur niveau de maîtrise - Choix du totem à garder avec recalcul des réussites - Conversion tous templates chat cards en .hbs - Fiches PNJ et Créature avec sélecteurs pour tous les niveaux - Documentation technique (ARCHITECTURE.md) et utilisateur (GUIDE_UTILISATEUR.md) - Mise à jour system.json pour compatibilité v14 - Tous les TODOs du README.md complétés Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
+186
-8
@@ -2,34 +2,97 @@ 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 }) {
|
||||
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--;
|
||||
modFormula = "(1D10cs>=" + difficulty + `[human_${game.user.name}]*2)`;
|
||||
|
||||
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 + "+(1D10cs>=" + difficulty + `[adapted_${game.user.name}]*2)`;
|
||||
modFormula = modFormula + "+" + adaptedFormula;
|
||||
} else {
|
||||
modFormula = "(1D10cs>=" + difficulty + `[adapted_${game.user.name}]*2)`;
|
||||
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 += (difficulty != undefined) ? "cs>=" + difficulty : "cs>=7";
|
||||
baseFormula += (adjustedDifficulty != undefined) ? "cs>=" + adjustedDifficulty : "cs>=7";
|
||||
baseFormula += `[regular_${game.user.name}]`
|
||||
|
||||
// Construction de la formule finale
|
||||
@@ -39,14 +102,129 @@ export class VermineUtils {
|
||||
|
||||
// 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, ...arguments);
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user