diff --git a/.gitignore b/.gitignore index bf1b95b..f4bdd7f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ styles/*.css node_modules/ .history +.github/ diff --git a/lethal-fantasy.mjs b/lethal-fantasy.mjs index e868730..e001bff 100644 --- a/lethal-fantasy.mjs +++ b/lethal-fantasy.mjs @@ -267,9 +267,13 @@ Hooks.on(hookName, (message, html, data) => { } // Envoyer le message socket à l'utilisateur contrôlant le combatant - const owners = game.users.filter(u => - combatant.actor.testUserPermission(u, "OWNER") + // Only consider active (online) users; fall back to any active GM for unowned/GM monsters. + let owners = game.users.filter(u => + u.active && combatant.actor.testUserPermission(u, "OWNER") ) + if (owners.length === 0) { + owners = game.users.filter(u => u.active && u.isGM) + } // Récupérer l'acteur attaquant pour vérifier qui l'a lancé const attacker = game.actors.get(attackerId) @@ -546,12 +550,27 @@ Hooks.on("createChatMessage", async (message) => { // Calculer les DR const armorDR = defender.computeDamageReduction() || 0 - - // Appliquer les dégâts avec armure DR par défaut const finalDamage = Math.max(0, damageTotal - armorDR) - await defender.applyDamage(-finalDamage) - // Créer un message de confirmation + // For unlinked tokens (default for monsters), we need the specific token actor, not the base + // world actor — otherwise applyDamage would modify the base actor and affect every unlinked + // copy of that monster. Prefer the combatant actor, fall back to canvas scan. + const defenderCombatant = game.combat?.combatants?.find(c => c.actorId === defender.id) + const defenderTokenId = defenderCombatant?.token?.id + ?? canvas.tokens?.placeables?.find(t => t.actor?.id === defender.id)?.id + ?? null + + // Apply damage. If the current user does not own the defender (e.g. player hitting a GM monster), + // route the HP update to the GM via socket. The confirmation message is still created here + // since all users can create chat messages. + if (defender.isOwner) { + const tokenActor = defenderCombatant?.actor ?? defender + await tokenActor.applyDamage(-finalDamage) + } else { + game.socket.emit(`system.${SYSTEM.id}`, { type: "applyDamage", actorId: defender.id, tokenId: defenderTokenId, damage: -finalDamage }) + } + + // Créer un message de confirmation (visible to GM only) const messageContent = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs", { @@ -566,7 +585,8 @@ Hooks.on("createChatMessage", async (message) => { await ChatMessage.create({ content: messageContent, - speaker: ChatMessage.getSpeaker({ actor: defender }) + speaker: ChatMessage.getSpeaker({ actor: defender }), + whisper: ChatMessage.getWhisperRecipients("GM") }) }) diff --git a/module/documents/roll.mjs b/module/documents/roll.mjs index 78cd800..f653036 100644 --- a/module/documents/roll.mjs +++ b/module/documents/roll.mjs @@ -681,7 +681,15 @@ export default class LethalFantasyRoll extends Roll { rejectClose: false // Click on Close button will not launch an error }) - let initRoll = new Roll(`min(${rollContext.initiativeDice}, ${options.maxInit})`, options.data, rollContext) + if (!rollContext) return + + // When the value is a plain number (e.g. "1" for Declared Ready on Alert), wrapping it in + // min(1, maxInit) produces a dice-less formula that FoundryVTT cannot evaluate to a valid + // total. Use the constant directly; min() is only needed for actual dice expressions. + const isDiceFormula = /[dD]/.test(rollContext.initiativeDice) + const formula = isDiceFormula ? `min(${rollContext.initiativeDice}, ${options.maxInit})` : rollContext.initiativeDice + + let initRoll = new Roll(formula, options.data) await initRoll.evaluate() let msg = await initRoll.toMessage({ flavor: `Initiative for ${options.actorName}` }, { rollMode: rollContext.visibility }) if (game?.dice3d) { @@ -690,7 +698,7 @@ export default class LethalFantasyRoll extends Roll { if (options.combatId && options.combatantId) { let combat = game.combats.get(options.combatId) - combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]); + await combat.updateEmbeddedDocuments("Combatant", [{ _id: options.combatantId, initiative: initRoll.total, 'system.progressionCount': 0 }]) } } diff --git a/module/utils.mjs b/module/utils.mjs index 7b97d18..7ecb38c 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -113,6 +113,16 @@ export default class LethalFantasyUtils { console.log(`handleSocketEvent !`, msg) let actor switch (msg.type) { + case "applyDamage": + if (game.user.isGM) { + // Prefer the specific token actor (correct for unlinked monsters); fall back to world actor. + actor = msg.tokenId + ? canvas.tokens?.placeables?.find(t => t.id === msg.tokenId)?.actor + : (game.combat?.combatants?.find(c => c.actorId === msg.actorId)?.actor + ?? game.actors.get(msg.actorId)) + if (actor) actor.applyDamage(msg.damage) + } + break case "rollInitiative": actor = game.actors.get(msg.actorId) actor.system.rollInitiative(msg.combatId, msg.combatantId)