import { SYSTEM } from "./config/system.mjs" // Map temporaire pour stocker les données d'attaque en attente de défense if (!globalThis.pendingDefenses) { globalThis.pendingDefenses = new Map() } export default class LethalFantasyUtils { /* -------------------------------------------- */ static async loadCompendiumData(compendium) { const pack = game.packs.get(compendium) return await pack?.getDocuments() ?? [] } /* -------------------------------------------- */ static async loadCompendium(compendium, filter = item => true) { let compendiumData = await LethalFantasyUtils.loadCompendiumData(compendium) return compendiumData.filter(filter) } /* -------------------------------------------- */ static pushCombatOptions(html, options) { options.push({ name: "Reset Progression", condition: true, icon: '', callback: target => { game.combat.resetProgression(target.data('combatant-id')); } }) } /* -------------------------------------------- */ static setHookListeners() { Hooks.on('renderTokenHUD', async (hud, html, token) => { // HP Loss Button (existing) const lossHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/loss-hp-hud.hbs', {}) $(html).find('div.left').append(lossHPButton); $(html).find('img.lethal-hp-loss-hud').click((event) => { event.preventDefault(); let hpMenu = $(html).find('.hp-loss-wrap')[0] if (hpMenu.classList.contains("hp-loss-hud-disabled")) { $(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-disabled'); } else { $(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled'); } }) $(html).find('.loss-hp-hud-click').click((event) => { event.preventDefault(); let hpLoss = event.currentTarget.dataset.hpValue; if (token) { let tokenFull = canvas.tokens.placeables.find(t => t.id === token._id); console.log(tokenFull, token) let actor = tokenFull.actor; actor.applyDamage(Number(hpLoss)); $(html).find('.hp-loss-wrap')[0].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[0].classList.add('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[1].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[1].classList.add('hp-loss-hud-disabled'); $(html).find('.hp-loss-wrap')[2].classList.remove('hp-loss-hud-active'); $(html).find('.hp-loss-wrap')[2].classList.add('hp-loss-hud-disabled'); } }) // HP Gain Button (new) const gainHPButton = await foundry.applications.handlebars.renderTemplate('systems/fvtt-lethal-fantasy/templates/gain-hp-hud.hbs', {}) $(html).find('div.left').append(gainHPButton); $(html).find('img.lethal-hp-gain-hud').click((event) => { event.preventDefault(); let hpMenu = $(html).find('.hp-gain-wrap')[0] if (hpMenu.classList.contains("hp-gain-hud-disabled")) { $(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-disabled'); } else { $(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled'); } }) $(html).find('.gain-hp-hud-click').click((event) => { event.preventDefault(); let hpGain = event.currentTarget.dataset.hpValue; if (token) { let tokenFull = canvas.tokens.placeables.find(t => t.id === token._id); console.log(tokenFull, token) let actor = tokenFull.actor; actor.applyDamage(Number(hpGain)); // Positive value to add HP $(html).find('.hp-gain-wrap')[0].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[0].classList.add('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[1].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[1].classList.add('hp-gain-hud-disabled'); $(html).find('.hp-gain-wrap')[2].classList.remove('hp-gain-hud-active'); $(html).find('.hp-gain-wrap')[2].classList.add('hp-gain-hud-disabled'); } }) }) } /* -------------------------------------------- */ static handleSocketEvent(msg = {}) { console.log(`handleSocketEvent !`, msg) let actor switch (msg.type) { case "rollInitiative": actor = game.actors.get(msg.actorId) actor.system.rollInitiative(msg.combatId, msg.combatantId) break case "rollProgressionDice": actor = game.actors.get(msg.actorId) actor.system.rollProgressionDice(msg.combatId, msg.combatantId, msg.rollProgressionCount) break case "requestDefense": // Vérifier si le message est destiné à cet utilisateur if (msg.userId === game.user.id) { LethalFantasyUtils.showDefenseRequest(msg) } break case "offerAttackerGrit": // Vérifier si le message est destiné à cet utilisateur if (msg.userId === game.user.id) { LethalFantasyUtils.handleAttackerGritOffer(msg) } break } } /* -------------------------------------------- */ static async handleAttackerGritOffer(msg) { const { attackerId, attackRoll, defenseRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId } = msg const attacker = game.actors.get(attackerId) if (!attacker) { console.warn("Attacker not found:", attackerId) return } const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus( attacker, attackRoll, defenseRoll, attackerName, defenderName ) const attackRollFinal = attackRoll + attackBonus // Maintenant créer le message de comparaison await LethalFantasyUtils.compareAttackDefense({ attackerName, attackerId, attackRoll: attackRollFinal, attackWeaponId, attackRollType, attackRollKey, defenderName, defenderId, defenseRoll }) } /* -------------------------------------------- */ static async showDefenseRequest(msg) { const attackerName = msg.attackerName const attackerId = msg.attackerId const defenderName = msg.defenderName const weaponName = msg.weaponName || "attack" const attackRoll = msg.attackRoll const attackWeaponId = msg.attackWeaponId const attackRollType = msg.attackRollType const attackRollKey = msg.attackRollKey const combatantId = msg.combatantId const tokenId = msg.tokenId // Récupérer le défenseur - essayer d'abord depuis le combat, puis depuis le token let defender = null if (game.combat && combatantId) { const combatant = game.combat.combatants.get(combatantId) if (combatant) { defender = combatant.actor } } // Si pas trouvé dans le combat, chercher le token directement if (!defender && tokenId) { const token = canvas.tokens.get(tokenId) if (token) { defender = token.actor } } if (!defender) { ui.notifications.error("Defender actor not found") return } const isMonster = defender.type === "monster" // Pour les monstres, récupérer les attaques activées if (isMonster) { const enabledAttacks = Object.entries(defender.system.attacks).filter(([key, attack]) => attack.enabled) if (enabledAttacks.length === 0) { ui.notifications.warn("No enabled attacks available for defense") return } // Créer le contenu du dialogue pour monstre let attacksHTML = enabledAttacks.map(([key, attack]) => `` ).join("") const content = `

${attackerName} attacks ${defenderName} with ${weaponName}!

Attack roll: ${attackRoll}

` // Afficher le dialogue const result = await foundry.applications.api.DialogV2.wait({ window: { title: "Defense Roll" }, classes: ["lethalfantasy"], content, buttons: [ { label: "Roll Defense", icon: "fa-solid fa-shield", callback: (event, button, dialog) => { const attackKey = button.form.elements.attackKey.value return attackKey }, }, ], rejectClose: false }) // Si l'utilisateur a validé, lancer le jet de défense if (result) { // Stocker temporairement les données pour le hook preCreateChatMessage game.lethalFantasy = game.lethalFantasy || {} game.lethalFantasy.nextDefenseData = { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId: defender.id } console.log("Storing defense data for monster:", defender.id) defender.system.prepareMonsterRoll("monster-defense", result) } return } // Pour les personnages, récupérer les armes équipées const equippedWeapons = defender.items.filter(i => i.type === "weapon" && i.system.equipped === true ) if (equippedWeapons.length === 0) { ui.notifications.warn("No equipped weapons for defense") return } // Créer le contenu du dialogue pour personnage let weaponsHTML = equippedWeapons.map(w => `` ).join("") const content = `

${attackerName} attacks ${defenderName} with ${weaponName}!

Attack roll: ${attackRoll}

` // Afficher le dialogue const result = await foundry.applications.api.DialogV2.wait({ window: { title: "Defense Roll" }, classes: ["lethalfantasy"], content, buttons: [ { label: "Roll Defense", icon: "fa-solid fa-shield", callback: (event, button, dialog) => { const weaponId = button.form.elements.weaponId.value return weaponId }, }, ], rejectClose: false }) // Si l'utilisateur a validé, lancer le jet de défense if (result) { // Stocker temporairement les données pour le hook preCreateChatMessage game.lethalFantasy = game.lethalFantasy || {} game.lethalFantasy.nextDefenseData = { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId: defender.id } console.log("Storing defense data for character:", defender.id) defender.prepareRoll("weapon-defense", result) } } /* -------------------------------------------- */ static async offerGritLuckBonus(defender, attackRoll, currentDefenseRoll, attackerName, defenderName) { let totalBonus = 0 let keepOffering = true while (keepOffering && currentDefenseRoll + totalBonus < attackRoll) { const currentGrit = defender.system.grit.current const currentLuck = defender.system.luck.current // Si plus de points disponibles, sortir if (currentGrit <= 0 && currentLuck <= 0) { break } const buttons = [] if (currentGrit > 0) { buttons.push({ action: "grit", label: `Spend 1 Grit (+1D6) [${currentGrit} left]`, icon: "fa-solid fa-fist-raised", callback: () => "grit" }) } if (currentLuck > 0) { buttons.push({ action: "luck", label: `Spend 1 Luck (+1D6) [${currentLuck} left]`, icon: "fa-solid fa-clover", callback: () => "luck" }) } buttons.push({ action: "continue", label: "Continue (no bonus)", icon: "fa-solid fa-forward", callback: () => "continue" }) const content = `

${attackerName} rolled ${attackRoll}

${defenderName} currently has ${currentDefenseRoll + totalBonus}

${totalBonus > 0 ? `

Bonus already added: +${totalBonus}

` : ''}

You are losing! Spend Grit or Luck to add 1D6 to your defense?

` const choice = await foundry.applications.api.DialogV2.wait({ window: { title: "Defend with Grit or Luck" }, classes: ["lethalfantasy"], content, buttons, rejectClose: false }) if (!choice || choice === "continue") { keepOffering = false break } // Lancer 1D6 const bonusRoll = new Roll("1d6") await bonusRoll.evaluate() if (game?.dice3d) { await game.dice3d.showForRoll(bonusRoll, game.user, true) } totalBonus += bonusRoll.total // Déduire le point de Grit ou Luck if (choice === "grit") { await defender.update({ "system.grit.current": currentGrit - 1 }) await ChatMessage.create({ content: `

${defenderName} spends 1 Grit and rolls ${bonusRoll.total}! (Total defense bonus: +${totalBonus})

`, speaker: ChatMessage.getSpeaker({ actor: defender }) }) } else if (choice === "luck") { await defender.update({ "system.luck.current": currentLuck - 1 }) await ChatMessage.create({ content: `

${defenderName} spends 1 Luck and rolls ${bonusRoll.total}! (Total defense bonus: +${totalBonus})

`, speaker: ChatMessage.getSpeaker({ actor: defender }) }) } } return totalBonus } /* -------------------------------------------- */ static async offerAttackerGritBonus(attacker, currentAttackRoll, defenseRoll, attackerName, defenderName) { let totalBonus = 0 let keepOffering = true while (keepOffering && currentAttackRoll + totalBonus <= defenseRoll) { const currentGrit = attacker.system.grit.current // Si plus de points de Grit disponibles, sortir if (currentGrit <= 0) { break } const buttons = [ { action: "grit", label: `Spend 1 Grit (+1D6) [${currentGrit} left]`, icon: "fa-solid fa-fist-raised", callback: () => "grit" }, { action: "continue", label: "Continue (no bonus)", icon: "fa-solid fa-forward", callback: () => "continue" } ] const content = `

${attackerName} currently has ${currentAttackRoll + totalBonus}

${defenderName} rolled ${defenseRoll}

${totalBonus > 0 ? `

Bonus already added: +${totalBonus}

` : ''}

You are losing! Spend Grit to add 1D6 to your attack?

` const choice = await foundry.applications.api.DialogV2.wait({ window: { title: "Attack with Grit" }, classes: ["lethalfantasy"], content, buttons, rejectClose: false }) if (!choice || choice === "continue") { keepOffering = false break } // Lancer 1D6 const bonusRoll = new Roll("1d6") await bonusRoll.evaluate() if (game?.dice3d) { await game.dice3d.showForRoll(bonusRoll, game.user, true) } totalBonus += bonusRoll.total // Déduire le point de Grit await attacker.update({ "system.grit.current": currentGrit - 1 }) await ChatMessage.create({ content: `

${attackerName} spends 1 Grit and rolls ${bonusRoll.total}! (Total attack bonus: +${totalBonus})

`, speaker: ChatMessage.getSpeaker({ actor: attacker }) }) } return totalBonus } /* -------------------------------------------- */ static async compareAttackDefense(data) { console.log("compareAttackDefense called with:", data) const isAttackWin = data.attackRoll > data.defenseRoll console.log("isAttackWin:", isAttackWin, "attackRoll:", data.attackRoll, "defenseRoll:", data.defenseRoll) let damageButton = "" if (isAttackWin && (data.attackWeaponId || data.attackRollKey)) { console.log("Creating damage button. defenderId:", data.defenderId) // Déterminer le type de dégâts à lancer if (data.attackRollType === "weapon-attack") { damageButton = `
` } else if (data.attackRollType === "monster-attack") { damageButton = `
` } } const resultMessage = `

Combat Result

Attacker
${data.attackerName}
${data.attackRoll}
VS
Defender
${data.defenderName}
${data.defenseRoll}
${isAttackWin ? ` ${data.attackerName} hits ${data.defenderName}!` : ` ${data.defenderName} parries the attack!` }
${damageButton}
` console.log("Creating combat result message...") await ChatMessage.create({ content: resultMessage, speaker: { alias: "Combat System" } }) console.log("Combat result message created!") } static registerHandlebarsHelpers() { Handlebars.registerHelper('isNull', function (val) { return val == null; }); Handlebars.registerHelper('match', function (val, search) { if (val && search) { return val?.match(search); } return false }); Handlebars.registerHelper('exists', function (val) { return val != null && val !== undefined; }); Handlebars.registerHelper('isEmpty', function (list) { if (list) return list.length === 0; else return false; }); Handlebars.registerHelper('notEmpty', function (list) { return list.length > 0; }); Handlebars.registerHelper('isNegativeOrNull', function (val) { return val <= 0; }); Handlebars.registerHelper('isNegative', function (val) { return val < 0; }); Handlebars.registerHelper('isPositive', function (val) { return val > 0; }); Handlebars.registerHelper('equals', function (val1, val2) { return val1 === val2; }); Handlebars.registerHelper('neq', function (val1, val2) { return val1 !== val2; }); Handlebars.registerHelper('gt', function (val1, val2) { return val1 > val2; }) Handlebars.registerHelper('lt', function (val1, val2) { return val1 < val2; }) Handlebars.registerHelper('gte', function (val1, val2) { return val1 >= val2; }) Handlebars.registerHelper('lte', function (val1, val2) { return val1 <= val2; }) Handlebars.registerHelper('and', function (val1, val2) { return val1 && val2; }) Handlebars.registerHelper('or', function (val1, val2) { return val1 || val2; }) Handlebars.registerHelper('or3', function (val1, val2, val3) { return val1 || val2 || val3; }) Handlebars.registerHelper('for', function (from, to, incr, block) { let accum = ''; for (let i = from; i < to; i += incr) accum += block.fn(i); return accum; }) Handlebars.registerHelper('not', function (cond) { return !cond; }) Handlebars.registerHelper('count', function (list) { return list.length; }) Handlebars.registerHelper('countKeys', function (obj) { return Object.keys(obj).length; }) Handlebars.registerHelper('isEnabled', function (configKey) { return game.settings.get("bol", configKey); }) Handlebars.registerHelper('split', function (str, separator, keep) { return str.split(separator)[keep]; }) // If you need to add Handlebars helpers, here are a few useful examples: Handlebars.registerHelper('concat', function () { let outStr = ''; for (let arg in arguments) { if (typeof arguments[arg] != 'object') { outStr += arguments[arg]; } } return outStr; }) Handlebars.registerHelper('add', function (a, b) { return parseInt(a) + parseInt(b); }); Handlebars.registerHelper('mul', function (a, b) { return parseInt(a) * parseInt(b); }) Handlebars.registerHelper('sub', function (a, b) { return parseInt(a) - parseInt(b); }) Handlebars.registerHelper('abbrev2', function (a) { return a.substring(0, 2); }) Handlebars.registerHelper('abbrev3', function (a) { return a.substring(0, 3); }) Handlebars.registerHelper('valueAtIndex', function (arr, idx) { return arr[idx]; }) Handlebars.registerHelper('includesKey', function (items, type, key) { return items.filter(i => i.type === type).map(i => i.system.key).includes(key); }) Handlebars.registerHelper('includes', function (array, val) { return array.includes(val); }) Handlebars.registerHelper('eval', function (expr) { return eval(expr); }) Handlebars.registerHelper('isOwnerOrGM', function (actor) { console.log("Testing actor", actor.isOwner, game.userId) return actor.isOwner || game.isGM; }) Handlebars.registerHelper('upperCase', function (text) { if (typeof text !== 'string') return text return text.toUpperCase() }) Handlebars.registerHelper('upperFirst', function (text) { if (typeof text !== 'string') return text return text.charAt(0).toUpperCase() + text.slice(1) }) Handlebars.registerHelper('upperFirstOnly', function (text) { if (typeof text !== 'string') return text return text.charAt(0).toUpperCase() }) // Handle v12 removal of this helper Handlebars.registerHelper('select', function (selected, options) { const escapedValue = RegExp.escape(Handlebars.escapeExpression(selected)); const rgx = new RegExp(' value=[\"\']' + escapedValue + '[\"\']'); const html = options.fn(this); return html.replace(rgx, "$& selected"); }); } static getLethargyDice(level) { for (let s of SYSTEM.SPELL_LETHARGY_DICE) { if (Number(level) <= s.maxLevel) { return s.dice } } } /* -------------------------------------------- */ static async applyDamage(message, event) { // Récupérer les données du message let combatantId = event.currentTarget.dataset.combatantId if (!combatantId || !game.combat) { ui.notifications.error("No combatant selected") return } let combatant = game.combat.combatants.get(combatantId) if (!combatant) { ui.notifications.error("Combatant not found") return } let targetActor = combatant.token?.actor || game.actors.get(combatant.actorId) if (!targetActor) { ui.notifications.error("Target actor not found") return } // Récupérer les données de dégâts du message let damageTotal = message.rolls[0]?.total || 0 let weaponName = message.rolls[0]?.options?.rollName || "Unknown Weapon" // Calculer les DR let armorDR = targetActor.computeDamageReduction() || 0 let shieldDR = targetActor.getShieldDR() || 0 let totalDR = armorDR + shieldDR // Créer le dialogue const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-lethal-fantasy/templates/apply-damage-dialog.hbs", { targetName: targetActor.name, weaponName: weaponName, damageTotal: damageTotal, armorDR: armorDR, shieldDR: shieldDR, totalDR: totalDR, damageNoDR: damageTotal, damageWithArmor: Math.max(0, damageTotal - armorDR), damageWithAll: Math.max(0, damageTotal - totalDR) } ) const result = await foundry.applications.api.DialogV2.wait({ window: { title: "Apply Damage" }, classes: ["lethalfantasy"], position: { width: 280 }, content, buttons: [ { action: "noDR", label: "No DR", callback: () => ({ drType: "none", damage: damageTotal }) }, { action: "armorDR", label: "With Armor DR", callback: () => ({ drType: "armor", damage: Math.max(0, damageTotal - armorDR) }) }, { action: "allDR", label: "With Armor + Shield DR", callback: () => ({ drType: "all", damage: Math.max(0, damageTotal - totalDR) }) }, { action: "cancel", label: "Cancel", callback: () => null } ], rejectClose: false }) if (result && result.damage !== undefined) { await targetActor.applyDamage(-result.damage) // Message de confirmation let drText = "" if (result.drType === "armor") { drText = `Armor DR: ${armorDR}` } else if (result.drType === "all") { drText = `Total DR: ${totalDR}` } const messageContent = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs", { targetName: targetActor.name, damage: result.damage, drText: drText, weaponName: weaponName } ) ChatMessage.create({ user: game.user.id, speaker: { alias: targetActor.name }, rollMode: "gmroll", content: messageContent }) } } }