/** * Gestion des ActiveEffects pour Mournblade CYD 2.0 * Ce module fournit des utilitaires pour créer, appliquer et gérer les effets actifs * sur les Acteurs et les Items. */ export class MournbladeCYD2Effects { /** * Initialise le système de gestion des effets */ static init() { console.log("MournbladeCYD2 | Initializing ActiveEffects management"); // Hook pour appliquer les modifications des effets Hooks.on("applyActiveEffect", (effect, change, current, delta, changes) => { return this._onApplyActiveEffect(effect, change, current, delta, changes); }); // Hook pour supprimer les modifications des effets Hooks.on("removeActiveEffect", (effect, change, current, delta, changes) => { return this._onRemoveActiveEffect(effect, change, current, delta, changes); }); } /** * Parse une valeur d'effet en nombre * Gère les strings comme "+2", "-3", "5" * @param {string|number} value - Valeur à parser * @returns {number} - Valeur numérique * @private */ static _parseEffectValue(value) { if (typeof value === 'number') return value; if (typeof value !== 'string') return 0; const trimmed = value.trim(); if (!trimmed) return 0; if (trimmed.startsWith('+')) { return parseFloat(trimmed.substring(1)) || 0; } else if (trimmed.startsWith('-')) { return -(parseFloat(trimmed.substring(1)) || 0); } return parseFloat(trimmed) || 0; } /** * Hook appelé lorsqu'un effet est appliqué * Permet de personnaliser le calcul des modifications * @private */ static _onApplyActiveEffect(effect, change, current, delta, changes) { // Pour Mournblade, nous voulons gérer les valeurs string (ex: "+1", "-2") // Convertir delta en nombre si nécessaire const numericDelta = this._parseEffectValue(delta); const numericCurrent = current != null ? Number(current) : 0; if (!isNaN(numericDelta) && !isNaN(numericCurrent)) { return numericCurrent + numericDelta; } // Si on ne peut pas calculer, retourner delta tel quel return delta; } /** * Hook appelé lorsqu'un effet est supprimé * @private */ static _onRemoveActiveEffect(effect, change, current, delta, changes) { // Logique inverse de l'application // Foundry gère déjà la suppression, ce hook est pour des calculs personnalisés const numericDelta = this._parseEffectValue(delta); const numericCurrent = current != null ? Number(current) : 0; if (!isNaN(numericDelta) && !isNaN(numericCurrent)) { return numericCurrent - numericDelta; } return current; } /* -------------------------------------------- */ /* Méthodes de création d'effets */ /* -------------------------------------------- */ /** * Crée un effet simple de bonus/malus à un attribut * @param {string} name - Nom de l'effet * @param {string} attribute - Attribut cible (adr, pui, cla, pre, tre, vigueur, etc.) * @param {number|string} value - Valeur du bonus/malus * @param {object} options - Options supplémentaires * @returns {Object|null} - Données de l'effet ou null */ static createSimpleEffect(name, attribute, value, options = {}) { // Validation des paramètres if (!name || typeof name !== "string") { console.warn("MournbladeCYD2 | Effect name must be a non-empty string"); return null; } if (value == null) { console.warn("MournbladeCYD2 | Effect value cannot be null or undefined"); return null; } const attributeKey = this.getAttributeKey(attribute); if (!attributeKey) { console.warn(`MournbladeCYD2 | Unknown attribute: ${attribute}`); return null; } // Normaliser la valeur en string const valueString = String(value).trim(); return { name: name.trim(), icon: options.icon || "systems/fvtt-mournblade-cyd-2-0/assets/icons/capacite.webp", description: (options.description || "").trim(), changes: [ { key: attributeKey, mode: CONST.ActiveEffect.MODES.ADD, value: valueString, priority: options.priority ?? 0 } ], disabled: Boolean(options.disabled), duration: options.duration || {}, origin: options.origin || null, tint: options.tint || "", transfer: options.transfer !== false, statuses: options.statuses || [], flags: options.flags || {} }; } /** * Crée un effet de bonus permanent * @param {string} name - Nom de l'effet * @param {string} attribute - Attribut cible * @param {number|string} value - Valeur du bonus * @returns {Object} - Données de l'effet */ static createPermanentEffect(name, attribute, value) { return this.createSimpleEffect(name, attribute, value, { duration: {}, type: "base" }); } /** * Crée un effet temporaire (rounds, turns, etc.) * @param {string} name - Nom de l'effet * @param {string} attribute - Attribut cible * @param {number|string} value - Valeur du bonus/malus * @param {string} durationType - Type de durée (rounds, turns, seconds, combat) * @param {number} durationValue - Valeur de la durée * @returns {Object} - Données de l'effet */ static createTemporaryEffect(name, attribute, value, durationType, durationValue) { return this.createSimpleEffect(name, attribute, value, { duration: { type: durationType, value: durationValue }, type: "temp" }); } /** * Crée un effet avec plusieurs modifications * @param {string} name - Nom de l'effet * @param {Array} changes - Array de modifications {key, mode, value} * @param {object} options - Options supplémentaires * @returns {Object} - Données de l'effet */ static createMultiEffect(name, changes, options = {}) { return { name: name, icon: options.icon || "systems/fvtt-mournblade-cyd-2-0/assets/icons/capacite.webp", description: options.description || "", changes: changes.map(c => ({ key: c.key, mode: c.mode || CONST.ActiveEffect.MODES.ADD, value: c.value.toString(), priority: c.priority || 0 })), disabled: options.disabled || false, duration: options.duration || {}, origin: options.origin || null, tint: options.tint || "", transfer: options.transfer !== false }; } /* -------------------------------------------- */ /* Méthodes d'application d'effets */ /* -------------------------------------------- */ /** * Applique un effet à un acteur * @param {Actor} actor - L'acteur cible * @param {Object|ActiveEffect} effectData - Données de l'effet ou instance ActiveEffect * @returns {Promise} - L'effet créé ou null */ static async applyEffectToActor(actor, effectData) { if (!actor || !actor.canUserModify(game.user, "update")) return null; let effect; if (effectData instanceof foundry.documents.ActiveEffect) { effect = effectData; } else if (effectData?.toObject) { effect = effectData; } else { effect = new CONFIG.ActiveEffect.documentClass(effectData); } try { const createdEffects = await actor.createEmbeddedDocuments("ActiveEffect", [effect.toObject()]); return createdEffects[0]; } catch (error) { console.error("MournbladeCYD2 | Failed to apply effect:", error); ui.notifications?.error( game.i18n?.localize("MOURNBLADECYD2.EFFECT.applyError") || `Erreur: Impossible d'appliquer l'effet (${error.message})` ); return null; } } /** * Applique les effets d'un item à un acteur * @param {Item} item - L'item source * @param {Actor} actor - L'acteur cible * @returns {Promise>} - Liste des effets appliqués */ static async applyItemEffectsToActor(item, actor) { if (!item?.effects?.length || !actor) return []; if (!actor.canUserModify(game.user, "update")) return []; const effectsToApply = []; for (const effectData of item.effects) { // Par défaut, appliquer automatiquement SAUF si explicitement désactivé const autoApply = effectData.getFlag("mournblade-cyd2", "autoApply"); if (autoApply !== false) { effectsToApply.push({ ...effectData.toObject(), origin: item.uuid, name: `${item.name}: ${effectData.name}` }); } } if (effectsToApply.length === 0) return []; try { const createdEffects = await actor.createEmbeddedDocuments("ActiveEffect", effectsToApply); return createdEffects; } catch (error) { console.error("MournbladeCYD2 | Failed to apply item effects:", error); ui.notifications?.error( game.i18n?.localize("MOURNBLADECYD2.EFFECT.applyItemError") || `Erreur: Impossible d'appliquer les effets de l'item` ); return []; } } /* -------------------------------------------- */ /* Méthodes de gestion d'effets */ /* -------------------------------------------- */ /** * Désactive un effet * @param {Actor|Item} owner - Le propriétaire de l'effet * @param {string} effectId - ID de l'effet * @returns {Promise} - L'effet désactivé */ static async disableEffect(owner, effectId) { if (!owner?.canUserModify(game.user, "update")) return null; const effect = owner.effects.get(effectId); if (!effect) return null; await effect.update({ disabled: true }); return effect; } /** * Active un effet * @param {Actor|Item} owner - Le propriétaire de l'effet * @param {string} effectId - ID de l'effet * @returns {Promise} - L'effet activé */ static async enableEffect(owner, effectId) { if (!owner?.canUserModify(game.user, "update")) return null; const effect = owner.effects.get(effectId); if (!effect) return null; await effect.update({ disabled: false }); return effect; } /** * Toggle l'état d'un effet (actif/désactivé) * @param {Actor|Item} owner - Le propriétaire de l'effet * @param {string} effectId - ID de l'effet * @returns {Promise} - L'effet togglé */ static async toggleEffect(owner, effectId) { if (!owner?.canUserModify(game.user, "update")) return null; const effect = owner.effects.get(effectId); if (!effect) return null; await effect.update({ disabled: !effect.disabled }); return effect; } /** * Supprime un effet * @param {Actor|Item} owner - Le propriétaire de l'effet * @param {string} effectId - ID de l'effet * @returns {Promise} - L'effet supprimé */ static async deleteEffect(owner, effectId) { if (!owner?.canUserModify(game.user, "delete")) return null; const effect = owner.effects.get(effectId); if (!effect) return null; await owner.deleteEmbeddedDocuments("ActiveEffect", [effectId]); return effect; } /* -------------------------------------------- */ /* Méthodes utilitaires */ /* -------------------------------------------- */ /** * Obtient la clé complète pour un attribut * @param {string} attribute - Attribut court (adr, pui, cla, pre, tre, vigueur, etc.) * @returns {string|null} - Clé complète ou null */ static getAttributeKey(attribute) { if (!attribute) return null; const config = game.system.mournbladecyd2?.config; if (!config?.effectAttributeKeys) return null; // Normaliser en minuscules pour correspondre à la config const normalizedAttribute = attribute.toLowerCase().trim(); return config.effectAttributeKeys[normalizedAttribute] || null; } /** * Obtient tous les attributs modifiables * @returns {Object} - Map des attributs courts vers les clés complètes */ static getAllAttributeKeys() { const config = game.system.mournbladecyd2?.config; return config?.effectAttributeKeys || {}; } /** * Obtient les effets actifs d'un acteur * @param {Actor} actor - L'acteur * @returns {Array} - Liste des effets actifs (non désactivés) */ static getActiveEffects(actor) { if (!actor?.effects) return []; return actor.effects.filter(e => !e.disabled); } /** * Obtient les effets désactivés d'un acteur * @param {Actor} actor - L'acteur * @returns {Array} - Liste des effets désactivés */ static getDisabledEffects(actor) { if (!actor?.effects) return []; return actor.effects.filter(e => e.disabled); } /** * Obtient les effets par origine * @param {Actor} actor - L'acteur * @param {string} originUuid - UUID de l'origine * @returns {Array} - Liste des effets de cette origine */ static getEffectsByOrigin(actor, originUuid) { if (!actor?.effects) return []; return actor.effects.filter(e => e.origin === originUuid); } /** * Obtient les effets temporaires en cours * @param {Actor} actor - L'acteur * @returns {Array} - Liste des effets temporaires actifs */ static getActiveTemporaryEffects(actor) { if (!actor?.effects) return []; return actor.effects.filter(e => !e.disabled && e.duration?.type); } /** * Calcule la valeur totale des modifications pour une clé donnée * @param {Actor} actor - L'acteur * @param {string} key - Clé à vérifier * @returns {number} - Somme des modifications */ static getTotalModificationForKey(actor, key) { if (!actor?.effects) return 0; let total = 0; for (const effect of actor.effects) { if (effect.disabled) continue; for (const change of effect.changes || []) { if (change.key === key && change.mode === CONST.ActiveEffect.MODES.ADD) { total += Number(change.value) || 0; } } } return total; } /** * Obtient toutes les modifications actives groupées par clé * @param {Actor} actor - L'acteur * @returns {Object} - Objet avec les clés et les valeurs totales */ static getAllActiveModifications(actor) { if (!actor?.effects) return {}; const modifications = {}; for (const effect of actor.effects) { if (effect.disabled) continue; for (const change of effect.changes || []) { if (!modifications[change.key]) { modifications[change.key] = { value: 0, effects: [] }; } // Appliquer selon le mode const numericValue = Number(change.value) || 0; switch (change.mode) { case CONST.ActiveEffect.MODES.ADD: modifications[change.key].value += numericValue; break; case CONST.ActiveEffect.MODES.OVERRIDE: modifications[change.key].value = numericValue; modifications[change.key].overridden = true; break; case CONST.ActiveEffect.MODES.MULTIPLY: // Ne peut pas être additionné, stocké séparément if (!modifications[change.key].multipliers) { modifications[change.key].multipliers = []; } modifications[change.key].multipliers.push(numericValue); break; } modifications[change.key].effects.push(effect.name); } } return modifications; } /* -------------------------------------------- */ /* Méthodes de création d'effets prédéfinis */ /* -------------------------------------------- */ /** * Crée un effet de bonus d'attribut * @param {string} attribute - Attribut (adr, pui, cla, pre, tre) * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createAttributeBonusEffect(attribute, value, source = "Effet") { const attrNames = { adr: "Adresse", pui: "Puissance", cla: "Clairvoyance", pre: "Présence", tre: "Trempe" }; return this.createSimpleEffect( `${source}: Bonus de ${attrNames[attribute] || attribute}`, attribute, `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/attributs.webp", type: "base" } ); } /** * Crée un effet de malus d'attribut * @param {string} attribute - Attribut * @param {number} value - Valeur du malus (positif) * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createAttributeMalusEffect(attribute, value, source = "Effet") { const attrNames = { adr: "Adresse", pui: "Puissance", cla: "Clairvoyance", pre: "Présence", tre: "Trempe" }; return this.createSimpleEffect( `${source}: Malus de ${attrNames[attribute] || attribute}`, attribute, `-${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/malus.webp", type: "base" } ); } /** * Crée un effet de bonus à la Vigueur * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createVigueurBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus de Vigueur`, "vigueur", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/vigueur.webp", type: "base" } ); } /** * Crée un effet de bonus au Seuil de Pouvoir * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createSeuilPouvoirBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus au Seuil de Pouvoir`, "seuilPouvoir", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/ame.webp", type: "base" } ); } /** * Crée un effet de bonus à la Bonne Aventure * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createBonneAventureBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus de Bonne Aventure`, "bonneAventure", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/bonneaventure.webp", type: "base" } ); } /** * Crée un effet de bonus à l'Initiative * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createInitiativeBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus d'Initiative`, "initiative", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/initiative.webp", type: "temp", duration: { type: "rounds", value: 1 } } ); } /** * Crée un effet de bonus à la Défense * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createDefenseBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus de Défense`, "defense", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/defense.webp", type: "temp", duration: { type: "rounds", value: 1 } } ); } /** * Crée un effet de bonus à la Protection * @param {number} value - Valeur du bonus * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createProtectionBonusEffect(value, source = "Effet") { return this.createSimpleEffect( `${source}: Bonus de Protection`, "protection", `+${value}`, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/protection.webp", type: "base" } ); } /* -------------------------------------------- */ /* Méthodes de gestion des statuts */ /* -------------------------------------------- */ /** * Crée un effet qui applique un statut * @param {string} status - Nom du statut * @param {string} source - Source de l'effet * @returns {Object} - Données de l'effet */ static createStatusEffect(status, source = "Effet") { return { name: `${source}: ${status}`, icon: `systems/fvtt-mournblade-cyd-2-0/assets/icons/status_${status.toLowerCase()}.webp`, description: `Applique le statut ${status}`, changes: [], statuses: [status], disabled: false, duration: {}, origin: null, tint: "", transfer: true }; } /** * Crée un effet d'adversité bleue * @param {number} value - Nombre d'adversités * @returns {Object|null} - Données de l'effet ou null */ static createAdversiteBleueEffect(value) { if (value == null) return null; return this.createSimpleEffect( `Adversité Bleue: +${value}`, "adversite.bleue", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_bleue.webp", duration: { type: "rounds", value: 1 }, statuses: ["adversite-bleue"] } ); } /** * Crée un effet d'adversité rouge * @param {number} value - Nombre d'adversités * @returns {Object|null} - Données de l'effet ou null */ static createAdversiteRougeEffect(value) { if (value == null) return null; return this.createSimpleEffect( `Adversité Rouge: +${value}`, "adversite.rouge", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_rouge.webp", duration: { type: "rounds", value: 1 }, statuses: ["adversite-rouge"] } ); } /** * Crée un effet d'adversité noire * @param {number} value - Nombre d'adversités * @returns {Object|null} - Données de l'effet ou null */ static createAdversiteNoireEffect(value) { if (value == null) return null; return this.createSimpleEffect( `Adversité Noire: +${value}`, "adversite.noire", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_noire.webp", duration: { type: "rounds", value: 1 }, statuses: ["adversite-noire"] } ); } /* -------------------------------------------- */ /* Méthodes pour les Runes */ /* -------------------------------------------- */ /** * Crée un effet de Rune prononcée * @param {Object} rune - Données de la rune * @param {number} pointsAme - Points de pouvoir dépensés * @returns {Object|null} - Données de l'effet ou null */ static createRunePrononceeEffect(rune, pointsAme) { if (!rune || !rune.name || pointsAme == null) return null; // Utiliser une icône par défaut si l'image de la rune est l'image par défaut const icon = rune.img?.includes('/blank.png') || !rune.img ? "systems/fvtt-mournblade-cyd-2-0/assets/icons/rune.webp" : rune.img; return { name: `Rune: ${rune.name} (Prononcée)`, icon: icon, description: rune.system?.description || "", changes: [], // Les modifications spécifiques peuvent être ajoutées par les appels disabled: false, duration: { type: "rounds", value: Math.ceil(pointsAme / 3) }, origin: rune.uuid || null, tint: "#00ff00", transfer: true, flags: { "mournblade-cyd2": { runeId: rune._id, runeType: "prononcee", pointsAme: pointsAme } } }; } /** * Crée un effet de Rune tracée * @param {Object} rune - Données de la rune * @param {number} pointsAme - Points de pouvoir dépensés * @returns {Object|null} - Données de l'effet ou null */ static createRuneTraceeEffect(rune, pointsAme) { if (!rune || !rune.name || pointsAme == null) return null; // Utiliser une icône par défaut si l'image de la rune est l'image par défaut const icon = rune.img?.includes('/blank.png') || !rune.img ? "systems/fvtt-mournblade-cyd-2-0/assets/icons/rune.webp" : rune.img; return { name: `Rune: ${rune.name} (Tracée)`, icon: icon, description: rune.system?.description || "", changes: [], // Les modifications spécifiques peuvent être ajoutées par les appels disabled: false, duration: { type: "rounds", value: Math.ceil(pointsAme / 3) * 2 }, origin: rune.uuid || null, tint: "#0000ff", transfer: true, flags: { "mournblade-cyd2": { runeId: rune._id, runeType: "tracee", pointsAme: pointsAme } } }; } } // Initialisation automatique Hooks.once("init", () => { MournbladeCYD2Effects.init(); });