Files
fvtt-mournblade-cyd-2-0/modules/mournblade-cyd2-effects.js
T
uberwald a1519e7a60 Fix: Replace missing effect.webp icon with existing capacite.webp
- effect.webp icon was missing, causing infinite 404 errors
- Replaced all references with capacite.webp which exists
- Fixed in base-actor-sheet.mjs, base-item-sheet.mjs, mournblade-cyd2-effects.js
- Fixed in partial-active-effects.hbs and partial-item-effects.hbs templates
- Updated test script to check for effect.webp references

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-07 00:36:12 +02:00

808 lines
25 KiB
JavaScript

/**
* 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<ActiveEffect|null>} - 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<Array<ActiveEffect>>} - 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<ActiveEffect|null>} - 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<ActiveEffect|null>} - 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<ActiveEffect|null>} - 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<ActiveEffect|null>} - 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<ActiveEffect>} - 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<ActiveEffect>} - 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<ActiveEffect>} - 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<ActiveEffect>} - 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();
});