/** * 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); }); } /** * 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 en nombre si nécessaire if (typeof current === "number" && typeof delta === "number") { return current + delta; } return delta; } /** * Hook appelé lorsqu'un effet est supprimé * @private */ static _onRemoveActiveEffect(effect, change, current, delta, changes) { // Logique inverse de l'application if (typeof current === "number" && typeof delta === "number") { return current - delta; } 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} - Données de l'effet */ static createSimpleEffect(name, attribute, value, options = {}) { const attributeKey = this.getAttributeKey(attribute); if (!attributeKey) { console.warn(`MournbladeCYD2 | Unknown attribute: ${attribute}`); return null; } return { name: name, icon: options.icon || "systems/fvtt-mournblade-cyd-2-0/assets/icons/effect.webp", description: options.description || "", changes: [ { key: attributeKey, mode: CONST.ActiveEffect.MODES.ADD, value: value.toString(), priority: options.priority || 0 } ], disabled: options.disabled || false, duration: options.duration || {}, origin: options.origin || null, tint: options.tint || "", transfer: options.transfer !== false }; } /** * 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/effect.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.isOwner) return null; const effect = effectData instanceof foundry.documents.ActiveEffect ? effectData : 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); 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 []; const effectsToApply = []; for (const effectData of item.effects) { // Vérifier si l'effet doit être appliqué automatiquement if (effectData.getFlag("mournblade-cyd2", "autoApply") !== false) { effectsToApply.push({ ...effectData.toObject(), origin: item.uuid, name: `${item.name}: ${effectData.name}` }); } } if (effectsToApply.length === 0) return []; const createdEffects = await actor.createEmbeddedDocuments("ActiveEffect", effectsToApply); return createdEffects; } /* -------------------------------------------- */ /* 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) { const effect = owner.effects.get(effectId); if (effect) { await effect.update({ disabled: true }); return effect; } return null; } /** * 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) { const effect = owner.effects.get(effectId); if (effect) { await effect.update({ disabled: false }); return effect; } return null; } /** * 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) { const effect = owner.effects.get(effectId); if (effect) { await effect.update({ disabled: !effect.disabled }); return effect; } return null; } /** * 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) { const effect = owner.effects.get(effectId); if (effect) { await owner.deleteEmbeddedDocuments("ActiveEffect", [effectId]); return effect; } return null; } /* -------------------------------------------- */ /* 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) { const config = game.system.mournbladecyd2?.config; if (!config?.effectAttributeKeys) return null; return config.effectAttributeKeys[attribute] || 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} - Données de l'effet */ static createAdversiteBleueEffect(value) { return this.createSimpleEffect( `Adversité Bleue: +${value}`, "adversite.bleue", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_bleue.webp", type: "temp", statuses: ["adversite-bleue"] } ); } /** * Crée un effet d'adversité rouge * @param {number} value - Nombre d'adversités * @returns {Object} - Données de l'effet */ static createAdversiteRougeEffect(value) { return this.createSimpleEffect( `Adversité Rouge: +${value}`, "adversite.rouge", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_rouge.webp", type: "temp", statuses: ["adversite-rouge"] } ); } /** * Crée un effet d'adversité noire * @param {number} value - Nombre d'adversités * @returns {Object} - Données de l'effet */ static createAdversiteNoireEffect(value) { return this.createSimpleEffect( `Adversité Noire: +${value}`, "adversite.noire", value, { icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/gemme_noire.webp", type: "temp", 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} - Données de l'effet */ static createRunePrononceeEffect(rune, pointsAme) { return { name: `Rune: ${rune.name} (Prononcée)`, icon: rune.img || "systems/fvtt-mournblade-cyd-2-0/assets/icons/rune.webp", description: rune.system.description || "", changes: [ // Ajouter ici les modifications spécifiques de la rune ], disabled: false, duration: { type: "rounds", value: Math.ceil(pointsAme / 3) }, origin: rune.uuid, 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} - Données de l'effet */ static createRuneTraceeEffect(rune, pointsAme) { return { name: `Rune: ${rune.name} (Tracée)`, icon: rune.img || "systems/fvtt-mournblade-cyd-2-0/assets/icons/rune.webp", description: rune.system.description || "", changes: [], disabled: false, duration: { type: "rounds", value: Math.ceil(pointsAme / 3) * 2 }, origin: rune.uuid, tint: "#0000ff", transfer: true, flags: { "mournblade-cyd2": { runeId: rune._id, runeType: "tracee", pointsAme: pointsAme } } }; } } // Initialisation automatique table Hooks.once("init", () => { MournbladeCYD2Effects.init(); });