From 156672d8534cac406d8f8e116f4883a060e885cb Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Sat, 6 Jun 2026 22:51:31 +0200 Subject: [PATCH] ActiveEffects: Add complete ActiveEffects management system - New: modules/mournblade-cyd2-effects.js with utility methods for creating, applying, and managing effects - New: templates/partial-active-effects.hbs for displaying actor effects - New: templates/partial-item-effects.hbs for displaying item effects - Update: modules/mournblade-cyd2-config.js with effect configuration (types, attribute keys, categories) - Update: templates/actor-sheet.hbs and creature-sheet.hbs with Effects tab - Update: templates/partial-item-nav.hbs with conditional Effects tab - Update: templates/item-talent-sheet.hbs with Effects tab content - Update: base-actor-sheet.mjs with effect action handlers (create, edit, delete, toggle) - Update: base-item-sheet.mjs with effect action handlers and context - Update: modules/mournblade-cyd2-main.js to import and expose MournbladeCYD2Effects - Update: lang/fr.json with effect-related translations Features: - Support for creating permanent and temporary effects - Support for attribute modifications (ADR, PUI, CLA, PRE, TRE, etc.) - Support for health, soul, combat, and adversity modifications - Support for status effects - Support for runes (pronounced and traced) effects - Toggle to enable/disable effects - Duration tracking (rounds, turns, seconds, combat, scene) - Display of all active modifications summary Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- lang/fr.json | 39 +- .../applications/sheets/base-actor-sheet.mjs | 120 +++ .../applications/sheets/base-item-sheet.mjs | 121 +++ modules/mournblade-cyd2-config.js | 90 +++ modules/mournblade-cyd2-effects.js | 712 ++++++++++++++++++ modules/mournblade-cyd2-main.js | 2 + templates/actor-sheet.hbs | 5 + templates/creature-sheet.hbs | 6 + templates/item-talent-sheet.hbs | 7 + templates/partial-active-effects.hbs | 111 +++ templates/partial-item-effects.hbs | 77 ++ templates/partial-item-nav.hbs | 3 + 12 files changed, 1291 insertions(+), 2 deletions(-) create mode 100644 modules/mournblade-cyd2-effects.js create mode 100644 templates/partial-active-effects.hbs create mode 100644 templates/partial-item-effects.hbs diff --git a/lang/fr.json b/lang/fr.json index 2bcbba9..b4fd34e 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -71,5 +71,40 @@ "traced": "Tracée", "tracedrune": "Rune tracée", "tracerune": "Tracer" - } -} \ No newline at end of file + }, + "EFFECT": { + "new": "Nouvel Effet", + "edit": "Éditer l'effet", + "delete": "Supprimer l'effet", + "deleteConfirm": "Supprimer l'effet", + "deleteConfirmText": "Êtes-vous sûr de vouloir supprimer cet effet ?", + "create": "Créer un effet", + "name": "Nom de l'effet", + "icon": "Icône", + "description": "Description", + "changes": "Modifications", + "addChange": "Ajouter une modification", + "duration": "Durée", + "durationType": "Type de durée", + "durationValue": "Valeur", + "disabled": "Désactivé", + "transfer": "Transférer au token", + "noDuration": "Aucune (permanent)", + "rounds": "Rounds", + "turns": "Tours", + "seconds": "Secondes", + "combat": "Jusqu'à la fin du combat", + "scene": "Jusqu'à la fin de la scène", + "attribute": "Attribut", + "value": "Valeur", + "mode": "Mode", + "modeAdd": "Ajouter", + "modeMultiply": "Multiplier", + "modeOverride": "Remplacer", + "modeUpgrade": "Améliorer", + "modeDowngrade": "Dégrader", + "activeEffects": "Effets Actifs", + "noActiveEffects": "Aucun effet actif", + "effectSummary": "Résumé des modifications", + "toggleEffect": "Activer/Désactiver" + } \ No newline at end of file diff --git a/modules/applications/sheets/base-actor-sheet.mjs b/modules/applications/sheets/base-actor-sheet.mjs index cd3276d..2f60dab 100644 --- a/modules/applications/sheets/base-actor-sheet.mjs +++ b/modules/applications/sheets/base-actor-sheet.mjs @@ -60,6 +60,12 @@ export default class MournbladeCYD2ActorSheetV2 extends HandlebarsApplicationMix rollDesengager: MournbladeCYD2ActorSheetV2.#onRollDesengager, rollInitiative: MournbladeCYD2ActorSheetV2.#onRollInitiative, rollFuir: MournbladeCYD2ActorSheetV2.#onRollFuir, + // Actions pour les ActiveEffects + createEffect: MournbladeCYD2ActorSheetV2.#onCreateEffect, + editEffect: MournbladeCYD2ActorSheetV2.#onEditEffect, + deleteEffect: MournbladeCYD2ActorSheetV2.#onDeleteEffect, + toggleEffect: MournbladeCYD2ActorSheetV2.#onToggleEffect, + applyEffect: MournbladeCYD2ActorSheetV2.#onApplyEffect, }, }; @@ -310,4 +316,118 @@ export default class MournbladeCYD2ActorSheetV2 extends HandlebarsApplicationMix static async #onRollFuir(event, target) { await this.document.rollFuir(); } + + // #region ActiveEffects Management + + /** + * Crée un nouvel effet actif + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onCreateEffect(event, target) { + event.preventDefault(); + + // Créer les données par défaut pour un nouvel effet + const defaultEffectData = { + name: game.i18n.localize("MOURNBLADECYD2.effect.new") || "Nouvel Effet", + icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/effect.webp", + description: "", + changes: [], + disabled: false, + duration: {}, + origin: this.document.uuid, + tint: "", + transfer: true, + flags: {} + }; + + // Utiliser la dialog native FoundryVTT pour créer l'effet + const effect = await foundry.applications.api.ActiveEffectDialog.create({ + document: this.document, + effect: defaultEffectData + }); + + if (effect) { + await this.document.createEmbeddedDocuments("ActiveEffect", [effect.toObject()]); + } + } + + /** + * Édite un effet actif existant + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onEditEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + // Ouvrir la sheet de l'effet pour édition + effect.sheet.render(true); + } + } + + /** + * Supprime un effet actif + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onDeleteEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + title: game.i18n.localize("MOURNBLADECYD2.effect.deleteConfirm") || "Supprimer l'effet", + content: game.i18n.localize("MOURNBLADECYD2.effect.deleteConfirmText") || `Êtes-vous sûr de vouloir supprimer l'effet "${effect.name}" ?` + }); + + if (confirmed) { + await this.document.deleteEmbeddedDocuments("ActiveEffect", [effectId]); + } + } + } + + /** + * Toggle l'état actif/désactivé d'un effet + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onToggleEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + await effect.update({ disabled: !effect.disabled }); + } + } + + /** + * Applique un effet à partir d'un item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onApplyEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + await effect.apply(); + } + } + + // #endregion } diff --git a/modules/applications/sheets/base-item-sheet.mjs b/modules/applications/sheets/base-item-sheet.mjs index 7abcdaa..4de7bc5 100644 --- a/modules/applications/sheets/base-item-sheet.mjs +++ b/modules/applications/sheets/base-item-sheet.mjs @@ -39,6 +39,12 @@ export default class MournbladeCYD2ItemSheetV2 extends HandlebarsApplicationMixi deletePredilection: MournbladeCYD2ItemSheetV2.#onDeletePredilection, addAutomation: MournbladeCYD2ItemSheetV2.#onAddAutomation, deleteAutomation: MournbladeCYD2ItemSheetV2.#onDeleteAutomation, + // Actions pour les ActiveEffects + createEffect: MournbladeCYD2ItemSheetV2.#onCreateEffect, + editEffect: MournbladeCYD2ItemSheetV2.#onEditEffect, + deleteEffect: MournbladeCYD2ItemSheetV2.#onDeleteEffect, + toggleEffect: MournbladeCYD2ItemSheetV2.#onToggleEffect, + applyEffect: MournbladeCYD2ItemSheetV2.#onApplyEffect, }, }; @@ -52,6 +58,7 @@ export default class MournbladeCYD2ItemSheetV2 extends HandlebarsApplicationMixi item: this.document, system: this.document.system, source: this.document.toObject(), + config: game.system.mournbladecyd2.config, enrichedDescription: await foundry.applications.ux.TextEditor.implementation.enrichHTML( this.document.system.description || "", { async: true } ), @@ -171,4 +178,118 @@ export default class MournbladeCYD2ItemSheetV2 extends HandlebarsApplicationMixi await this.document.update({ "system.isautomated": false }); } } + + // #region ActiveEffects Management + + /** + * Crée un nouvel effet actif sur l'item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onCreateEffect(event, target) { + event.preventDefault(); + + // Créer les données par défaut pour un nouvel effet + const defaultEffectData = { + name: game.i18n.localize("MOURNBLADECYD2.EFFECT.new") || "Nouvel Effet", + icon: "systems/fvtt-mournblade-cyd-2-0/assets/icons/effect.webp", + description: "", + changes: [], + disabled: false, + duration: {}, + origin: this.document.uuid, + tint: "", + transfer: false, + flags: {} + }; + + // Utiliser la dialog native FoundryVTT pour créer l'effet + const effect = await foundry.applications.api.ActiveEffectDialog.create({ + document: this.document, + effect: defaultEffectData + }); + + if (effect) { + await this.document.createEmbeddedDocuments("ActiveEffect", [effect.toObject()]); + } + } + + /** + * Édite un effet actif existant sur l'item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onEditEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + // Ouvrir la sheet de l'effet pour édition + effect.sheet.render(true); + } + } + + /** + * Supprime un effet actif de l'item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onDeleteEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + const confirmed = await foundry.applications.api.DialogV2.confirm({ + title: game.i18n.localize("MOURNBLADECYD2.EFFECT.deleteConfirm") || "Supprimer l'effet", + content: game.i18n.localize("MOURNBLADECYD2.EFFECT.deleteConfirmText") || `Êtes-vous sûr de vouloir supprimer l'effet "${effect.name}" ?` + }); + + if (confirmed) { + await this.document.deleteEmbeddedDocuments("ActiveEffect", [effectId]); + } + } + } + + /** + * Toggle l'état actif/désactivé d'un effet sur l'item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onToggleEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + await effect.update({ disabled: !effect.disabled }); + } + } + + /** + * Applique un effet à partir de l'item + * @param {Event} event - Événement + * @param {HTMLElement} target - Éléments cible + * @private + */ + static async #onApplyEffect(event, target) { + event.preventDefault(); + const effectId = target?.dataset?.effectId; + if (!effectId) return; + + const effect = this.document.effects.get(effectId); + if (effect) { + await effect.apply(); + } + } + + // #endregion } diff --git a/modules/mournblade-cyd2-config.js b/modules/mournblade-cyd2-config.js index 0ce4fc6..3850d67 100644 --- a/modules/mournblade-cyd2-config.js +++ b/modules/mournblade-cyd2-config.js @@ -119,6 +119,96 @@ export const MOURNBLADECYD2_CONFIG = { { key: "personnage", label: "Personnage" }, { key: "traitespece", label: "Trait d'espèce" } ], + + // Configuration des ActiveEffects + effectTypes: { + bonus: "Bonus", + malus: "Malus", + rune: "Effet de Rune", + don: "Effet de Don", + talent: "Effet de Talent", + trait: "Effet de Trait", + temporaire: "Effet Temporaire", + permanent: "Effet Permanent" + }, + + // Clés des attributs pour les modifications d'effets + effectAttributeKeys: { + // Attributs + adr: "system.attributs.adr.value", + pui: "system.attributs.pui.value", + cla: "system.attributs.cla.value", + pre: "system.attributs.pre.value", + tre: "system.attributs.tre.value", + + // Santé + vigueur: "system.sante.vigueur", + etat: "system.sante.etat", + nbcombativite: "system.sante.nbcombativite", + + // Âme + nbame: "system.ame.nbame", + seuilpouvoir: "system.ame.seuilpouvoir", + etatAme: "system.ame.etat", + + // Bonne Aventure + bonneaventure: "system.bonneaventure.base", + bonneaventureActuelle: "system.bonneaventure.actuelle", + eclat: "system.eclat.value", + + // Combat + initiative: "system.combat.inittotal", + defense: "system.combat.defensetotal", + protection: "system.combat.protectiontotal", + + // Adversités + adversiteBleue: "system.adversite.bleue", + adversiteRouge: "system.adversite.rouge", + adversiteNoire: "system.adversite.noire", + + // Balance + loi: "system.balance.loi", + chaos: "system.balance.chaos", + aspect: "system.balance.aspect", + + // Ressources + ressources: "system.ressources.value", + + // Vitesse + vitesse: "system.vitesse.value" + }, + + // Types de bonus/malus supportés (groupés par catégorie) + effectTypesConfig: { + attribut: { + label: "Attribut", + keys: ["adr", "pui", "cla", "pre", "tre"] + }, + sante: { + label: "Santé", + keys: ["vigueur", "etat", "nbcombativite"] + }, + ame: { + label: "Âme", + keys: ["nbame", "seuilpouvoir", "etatAme"] + }, + combat: { + label: "Combat", + keys: ["initiative", "defense", "protection"] + }, + bonneAventure: { + label: "Bonne Aventure", + keys: ["bonneaventure", "bonneaventureActuelle", "eclat"] + }, + adversite: { + label: "Adversité", + keys: ["adversiteBleue", "adversiteRouge", "adversiteNoire"] + }, + balance: { + label: "Balance", + keys: ["loi", "chaos", "aspect"] + } + }, optionsUseTalent: [ { key: "permanent", label: "Permanent" }, { key: "sceance", label: "Une fois par scéance" }, diff --git a/modules/mournblade-cyd2-effects.js b/modules/mournblade-cyd2-effects.js new file mode 100644 index 0000000..bba6b7b --- /dev/null +++ b/modules/mournblade-cyd2-effects.js @@ -0,0 +1,712 @@ +/** + * 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(); +}); diff --git a/modules/mournblade-cyd2-main.js b/modules/mournblade-cyd2-main.js index 769d9dc..523a70a 100644 --- a/modules/mournblade-cyd2-main.js +++ b/modules/mournblade-cyd2-main.js @@ -16,6 +16,7 @@ import { MournbladeCYD2Item } from "./mournblade-cyd2-item.js"; import { MournbladeCYD2Automation } from "./mournblade-cyd2-automation.js"; import { MournbladeCYD2TokenHud } from "./mournblade-cyd2-hud.js"; import { MOURNBLADECYD2_CONFIG } from "./mournblade-cyd2-config.js"; +import { MournbladeCYD2Effects } from "./mournblade-cyd2-effects.js"; // Import DataModels import * as models from "./models/index.mjs"; @@ -77,6 +78,7 @@ Hooks.once("init", async function () { game.system.mournbladecyd2 = { MournbladeCYD2Utility, MournbladeCYD2Automation, + MournbladeCYD2Effects, config: MOURNBLADECYD2_CONFIG } diff --git a/templates/actor-sheet.hbs b/templates/actor-sheet.hbs index 2fc4eb2..f38b2f0 100644 --- a/templates/actor-sheet.hbs +++ b/templates/actor-sheet.hbs @@ -115,6 +115,7 @@ Dons & Pactes Combat {{localize "MNBL.equipment"}} + Effets Bio&Notes
@@ -690,6 +691,10 @@ + {{!-- Effects Tab --}} +
+ {{> systems/fvtt-mournblade-cyd-2-0/templates/partial-active-effects.hbs}} +
{{!-- Biography Tab --}}
diff --git a/templates/creature-sheet.hbs b/templates/creature-sheet.hbs index 970d8db..bf642e8 100644 --- a/templates/creature-sheet.hbs +++ b/templates/creature-sheet.hbs @@ -92,6 +92,7 @@ Compétences Talents Armes + Effets Bio&Notes
@@ -422,6 +423,11 @@
+ {{!-- Effects Tab --}} +
+ {{> systems/fvtt-mournblade-cyd-2-0/templates/partial-active-effects.hbs}} +
+ {{!-- Bio&Notes Tab --}}
diff --git a/templates/item-talent-sheet.hbs b/templates/item-talent-sheet.hbs index 7e0a742..8b69be3 100644 --- a/templates/item-talent-sheet.hbs +++ b/templates/item-talent-sheet.hbs @@ -54,5 +54,12 @@
+ {{!-- Effects Tab --}} + {{#if item.effects.length}} +
+ {{> systems/fvtt-mournblade-cyd-2-0/templates/partial-item-effects.hbs}} +
+ {{/if}} + \ No newline at end of file diff --git a/templates/partial-active-effects.hbs b/templates/partial-active-effects.hbs new file mode 100644 index 0000000..6a1f4d4 --- /dev/null +++ b/templates/partial-active-effects.hbs @@ -0,0 +1,111 @@ +{{!-- Partial pour l'affichage des ActiveEffects --}} + +
+
    + + {{!-- En-tête --}} +
  • + +

    +
    +
     
    +
    + + + +
    +
  • + + {{!-- Affiche un message si aucun effet --}} + {{#if (not actor.effects.length)}} +
  • + Aucun effet actif +
  • + {{/if}} + + {{!-- Liste des effets --}} + {{#each actor.effects as |effect|}} +
  • + {{!-- Icône de l'effet --}} + + + {{!-- Nom et description de l'effet --}} +
    + + {{effect.name}} + {{#if effect.disabled}}{{/if}} + + + {{!-- Affichage compact des modifications --}} + {{#if effect.changes.length}} + + {{#each effect.changes as |change index|}} + {{#if (eq change.mode 0)}}(+{{/if}}{{#if (eq change.mode 1)}}(*{{/if}}{{#if (eq change.mode 2)}}={{/if}}{{change.value}}{{#if (eq change.mode 0)}}){{/if}}{{#if (eq change.mode 1)}}){{/if}} + {{#unless (eq index (subtract effect.changes.length 1))}}, {{/unless}} + {{/each}} + {{#each effect.statuses as |status|}} + {{#if (and (ne status "") (not (eq index 0)))}}, {{/if}} + + {{/each}} + + {{/if}} +
    + + {{!-- Affichage de la durée --}} + {{#if effect.duration.type}} + + {{#if (eq effect.duration.type "rounds")}}🔄{{/if}} + {{#if (eq effect.duration.type "turns")}}🎭{{/if}} + {{#if (eq effect.duration.type "seconds")}}⏱️{{/if}} + {{#if (eq effect.duration.type "combat")}}⚔️{{/if}} + {{#if (eq effect.duration.type "scene")}}📜{{/if}} + {{effect.duration.value}} + + {{/if}} + + {{!-- Contrôles --}} + +
  • + {{/each}} + +
+
+ +{{!-- Affichage détaillé des effets actifs --}} +{{#if actor.effects.length}} +
+

Résumé des modifications

+
+ {{#each actor.effects as |effect|}} + {{#if (not effect.disabled)}} + {{#if effect.changes.length}} +
+ {{effect.name}}: + {{#each effect.changes as |change|}} + + {{change.key}}: + {{#if (eq change.mode 0)}}+{{/if}} + {{#if (eq change.mode 1)}}*{{/if}} + {{#if (eq change.mode 2)}}={{/if}} + {{change.value}} + {{#if (eq change.mode 0)}}{{/if}} + {{#if (eq change.mode 1)}}{{/if}} + {{#unless @last}}, {{/unless}} + {{/each}} +
+ {{/if}} + {{/if}} + {{/each}} +
+
+{{/if}} diff --git a/templates/partial-item-effects.hbs b/templates/partial-item-effects.hbs new file mode 100644 index 0000000..65e9c41 --- /dev/null +++ b/templates/partial-item-effects.hbs @@ -0,0 +1,77 @@ +{{!-- Partial pour l'affichage des effets sur les items --}} + +
+
    + + {{!-- En-tête --}} +
  • + +

    +
    +
     
    +
    + + + +
    +
  • + + {{!-- Affiche un message si aucun effet --}} + {{#if (not item.effects.length)}} +
  • + Aucun effet sur cet item +
  • + {{/if}} + + {{!-- Liste des effets --}} + {{#each item.effects as |effect|}} +
  • + {{!-- Icône de l'effet --}} + + + {{!-- Nom et description de l'effet --}} +
    + + {{effect.name}} + {{#if effect.disabled}}{{/if}} + + + {{!-- Affichage compact des modifications --}} + {{#if effect.changes.length}} + + {{#each effect.changes as |change index|}} + {{change.key}}: {{change.value}} + {{#unless (eq index (subtract effect.changes.length 1))}}, {{/unless}} + {{/each}} + + {{/if}} +
    + + {{!-- Affichage de la durée --}} + {{#if effect.duration.type}} + + {{#if (eq effect.duration.type "rounds")}}🔄{{/if}} + {{#if (eq effect.duration.type "turns")}}🎭{{/if}} + {{#if (eq effect.duration.type "seconds")}}⏱️{{/if}} + {{#if (eq effect.duration.type "combat")}}⚔️{{/if}} + {{effect.duration.value}} + + {{/if}} + + {{!-- Contrôles --}} + +
  • + {{/each}} + +
+
diff --git a/templates/partial-item-nav.hbs b/templates/partial-item-nav.hbs index b754422..8e983c3 100644 --- a/templates/partial-item-nav.hbs +++ b/templates/partial-item-nav.hbs @@ -3,5 +3,8 @@