/** * Lethal Fantasy RPG System * Author: LeRatierBretonnien/Uberwald */ import { SYSTEM } from "./module/config/system.mjs" globalThis.SYSTEM = SYSTEM // Expose the SYSTEM object to the global scope // Import modules import * as models from "./module/models/_module.mjs" import * as documents from "./module/documents/_module.mjs" import * as applications from "./module/applications/_module.mjs" import { LethalFantasyCombatTracker, LethalFantasyCombat } from "./module/applications/combat.mjs" import { Macros } from "./module/macros.mjs" import { setupTextEnrichers } from "./module/enrichers.mjs" import { default as LethalFantasyUtils } from "./module/utils.mjs" export class ClassCounter { static printHello() { console.log("Hello") } static sendJsonPostRequest(e, s) { const t = { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(s) }; return fetch(e, t).then((e => { if (!e.ok) throw new Error("La requête a échoué avec le statut " + e.status); return e.json() })).catch((e => { throw console.error("Erreur envoi de la requête:", e), e })) } static registerUsageCount(e = game.system.id, s = {}) { if (game.user.isGM) { game.settings.register(e, "world-key", { name: "Unique world key", scope: "world", config: !1, default: "", type: String }); let t = game.settings.get(e, "world-key"); null != t && "" != t && "NONE" != t && "none" != t.toLowerCase() || (t = foundry.utils.randomID(32), game.settings.set(e, "world-key", t)); let a = { name: e, system: game.system.id, worldKey: t, version: game.system.version, language: game.settings.get("core", "language"), remoteAddr: game.data.addresses.remote, nbInstalledModules: game.modules.size, nbActiveModules: game.modules.filter((e => e.active)).length, nbPacks: game.world.packs.size, nbUsers: game.users.size, nbScenes: game.scenes.size, nbActors: game.actors.size, nbPlaylist: game.playlists.size, nbTables: game.tables.size, nbCards: game.cards.size, optionsData: s, foundryVersion: `${game.release.generation}.${game.release.build}` }; this.sendJsonPostRequest("https://www.uberwald.me/fvtt_appcount/count_post.php", a) } } } Hooks.once("init", function () { console.info("Lethal Fantasy RPG | Initializing System") console.info(SYSTEM.ASCII) globalThis.lethalFantasy = game.system game.system.CONST = SYSTEM // Expose the system API game.system.api = { applications, models, documents, } CONFIG.ui.combat = LethalFantasyCombatTracker CONFIG.Combat.documentClass = LethalFantasyCombat; CONFIG.Actor.documentClass = documents.LethalFantasyActor CONFIG.Actor.dataModels = { character: models.LethalFantasyCharacter, monster: models.LethalFantasyMonster, } CONFIG.Item.documentClass = documents.LethalFantasyItem CONFIG.Item.dataModels = { skill: models.LethalFantasySkill, gift: models.LethalFantasyGift, weapon: models.LethalFantasyWeapon, armor: models.LethalFantasyArmor, shield: models.LethalFantasyShield, spell: models.LethalFantasySpell, vulnerability: models.LethalFantasyVulnerability, equipment: models.LethalFantasyEquipment, miracle: models.LethalFantasyMiracle } // Register sheet application classes foundry.documents.collections.Actors.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyCharacterSheet, { types: ["character"], makeDefault: true }) foundry.documents.collections.Actors.registerSheet("lethalFantasy", applications.LethalFantasyMonsterSheet, { types: ["monster"], makeDefault: true }) foundry.documents.collections.Items.unregisterSheet("core", foundry.appv1.sheets.ActorSheet) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasySkillSheet, { types: ["skill"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyGiftSheet, { types: ["gift"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyVulnerabilitySheet, { types: ["vulnerability"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyWeaponSheet, { types: ["weapon"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasySpellSheet, { types: ["spell"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyArmorSheet, { types: ["armor"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyShieldSheet, { types: ["shield"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyEquipmentSheet, { types: ["equipment"], makeDefault: true }) foundry.documents.collections.Items.registerSheet("lethalFantasy", applications.LethalFantasyMiracleSheet, { types: ["miracle"], makeDefault: true }) // Other Document Configuration CONFIG.ChatMessage.documentClass = documents.LethalFantasyChatMessage // Dice system configuration CONFIG.Dice.rolls.push(documents.LethalFantasyRoll) game.settings.register("lethalFantasy", "worldKey", { name: "Unique world key", scope: "world", config: false, type: String, default: "", }) // Activate socket handler game.socket.on(`system.${SYSTEM.id}`, LethalFantasyUtils.handleSocketEvent) setupTextEnrichers() LethalFantasyUtils.registerHandlebarsHelpers() LethalFantasyUtils.setHookListeners() console.info("LETHAL FANTASY | System Initialized") }) /** * Perform one-time configuration of system configuration objects.f */ function preLocalizeConfig() { const localizeConfigObject = (obj, keys) => { for (let o of Object.values(obj)) { for (let k of keys) { o[k] = game.i18n.localize(o[k]) } } } } Hooks.once("ready", function () { console.info("LETHAL FANTASY | Ready") // Initialiser la table des résultats D30 documents.D30Roll.initialize() if (!SYSTEM.DEV_MODE) { registerWorldCount("lethalFantasy") } _showUserGuide() /** * */ async function _showUserGuide() { if (game.user.isGM) { const newVer = game.system.version } } }) // Test if version below 13 let hookName = "renderChatMessage" if (foundry.utils.isNewerVersion(game.version, "12.0",)) { hookName = "renderChatMessageHTML" } Hooks.on(hookName, (message, html, data) => { const typeMessage = data.message.flags.lethalFantasy?.typeMessage // Message de demande de jet de dés if (typeMessage === "askRoll") { // Affichage des boutons de jet de dés uniquement pour les joueurs if (game.user.isGM) { html.find(".ask-roll-dice").each((i, btn) => { btn.style.display = "none" }) } else { html.find(".ask-roll-dice").click((event) => { const btn = $(event.currentTarget) const type = btn.data("type") const value = btn.data("value") const avantage = btn.data("avantage") ?? "=" const character = game.user.character if (type === SYSTEM.ROLL_TYPE.RESOURCE) character.rollResource(value) else if (type === SYSTEM.ROLL_TYPE.SAVE) character.rollSave(value, avantage) }) } } // Gestion du survol et du clic sur les boutons de dégâts pour les GMs if (game.user.isGM) { // Show damage buttons only for GM $(html).find(".li-apply-wounds").each((i, btn) => { btn.style.display = "block" }) $(html).find(".apply-wounds-btn").hover( function (event) { // Mouse enter - select the token and pan to it let combatantId = $(this).data("combatant-id") if (combatantId && game.combat) { let combatant = game.combat.combatants.get(combatantId) if (combatant?.token) { let token = canvas.tokens.get(combatant.token.id) if (token) { token.control({ releaseOthers: true }) canvas.animatePan(token.center) } } } }, function (event) { // Mouse leave - release selection canvas.tokens.releaseAll() } ) $(html).find(".apply-wounds-btn").click((event) => { LethalFantasyUtils.applyDamage(message, event) }) } // Gestion du survol et du clic sur les boutons de défense $(html).find(".request-defense-btn").hover( function (event) { // Mouse enter - select the token and pan to it let tokenId = $(this).data("token-id") if (tokenId) { let token = canvas.tokens.get(tokenId) if (token) { token.control({ releaseOthers: true }) canvas.animatePan(token.center) } } }, function (event) { // Mouse leave - release selection canvas.tokens.releaseAll() } ) // Gestionnaire pour les boutons de demande de défense $(html).find(".request-defense-btn").off("click").on("click", (event) => { event.preventDefault() event.stopPropagation() const button = $(event.currentTarget) const combatantId = button.data("combatant-id") const tokenId = button.data("token-id") // Récupérer le combattant soit du combat, soit directement du token let combatant = null let token = null if (game.combat && combatantId) { combatant = game.combat.combatants.get(combatantId) } // Si pas de combattant trouvé, chercher le token directement if (!combatant && tokenId) { token = canvas.tokens.get(tokenId) if (token) { // Créer un pseudo-combattant avec les infos du token combatant = { actor: token.actor, name: token.name, token: token, actorId: token.actorId } } } if (!combatant) return // Récupérer les informations de l'attaquant depuis le message const attackerName = message.rolls[0]?.actorName || "Unknown" const attackerId = message.rolls[0]?.actorId const weaponName = message.rolls[0]?.rollName || "weapon" const attackRoll = message.rolls[0]?.rollTotal || 0 const defenderName = combatant.name const attackWeaponId = message.rolls[0]?.rollTarget?.weapon?.id || message.rolls[0]?.rollTarget?.weapon?._id const attackRollType = message.rolls[0]?.type const attackRollKey = message.rolls[0]?.rollTarget?.rollKey // Préparer le message de demande de défense const defenseMsg = { type: "requestDefense", attackerName: attackerName, attackerId: attackerId, defenderName: defenderName, weaponName: weaponName, attackRoll: attackRoll, attackWeaponId: attackWeaponId, attackRollType: attackRollType, attackRollKey: attackRollKey, combatantId: combatantId, tokenId: tokenId } // Envoyer le message socket à l'utilisateur contrôlant le combatant const owners = game.users.filter(u => combatant.actor.testUserPermission(u, "OWNER") ) // Récupérer l'acteur attaquant pour vérifier qui l'a lancé const attacker = game.actors.get(attackerId) const attackerOwners = attacker ? game.users.filter(u => attacker.testUserPermission(u, "OWNER")).map(u => u.id) : [] let messageSent = false owners.forEach(owner => { // Ne pas afficher le dialogue à l'attaquant lui-même s'il contrôle aussi le défenseur if (attackerOwners.includes(owner.id) && owner.id === game.user.id) { // Ne rien faire - on ne veut pas que l'attaquant se défende contre lui-même return } if (owner.id === game.user.id) { // Si l'utilisateur actuel est le propriétaire du défenseur (mais pas l'attaquant), appeler directement LethalFantasyUtils.showDefenseRequest({ ...defenseMsg, userId: owner.id }) messageSent = true } else { // Sinon, envoyer via socket game.socket.emit(`system.${SYSTEM.id}`, { ...defenseMsg, userId: owner.id }) messageSent = true } }) // Notification pour l'attaquant if (messageSent) { ui.notifications.info(`Defense request sent to ${defenderName}'s controller`) } }) // Gestionnaire pour les boutons de jet de dégâts (armes et résultats de combat) $(html).find(".damage-roll-btn, .roll-damage-btn").off("click").on("click", async (event) => { event.preventDefault() event.stopPropagation() const button = $(event.currentTarget) const weaponId = button.data("weapon-id") const attackKey = button.data("attack-key") let attackerId = button.data("attacker-id") const defenderId = button.data("defender-id") const damageType = button.data("damage-type") const damageFormula = button.data("damage-formula") const damageModifier = button.data("damage-modifier") const isMonster = button.data("is-monster") // Récupérer l'acteur (soit depuis le message, soit depuis attackerId) let actor = attackerId ? game.actors.get(attackerId) : game.actors.get(message.rolls[0]?.actorId) if (!actor) { ui.notifications.error("Actor not found") return } // Pour les boutons de résultat de combat (monster damage) if (damageType === "monster" && attackKey) { await actor.system.prepareMonsterRoll("monster-damage", attackKey, undefined, undefined, undefined, defenderId) return } // Pour les monstres, utiliser prepareMonsterRoll if (isMonster || actor.type === "monster") { await actor.system.prepareMonsterRoll("monster-damage", weaponId, undefined, undefined, damageModifier) return } // Pour les personnages, récupérer l'arme const weapon = actor.items.get(weaponId) if (!weapon) { ui.notifications.error("Weapon not found") return } // Lancer les dégâts avec la bonne méthode const rollType = damageType === "small" ? "weapon-damage-small" : "weapon-damage-medium" await actor.prepareRoll(rollType, weaponId, undefined, defenderId) }) // Masquer les boutons de dommages dans les messages de résultat de combat si l'utilisateur n'est pas l'attaquant $(html).find(".roll-damage-btn").each(function() { const button = $(this) const attackerId = button.data("attacker-id") if (attackerId) { const attacker = game.actors.get(attackerId) // Masquer le bouton si l'utilisateur n'est pas GM et ne possède pas l'attaquant if (!game.user.isGM && !attacker?.testUserPermission(game.user, "OWNER")) { button.hide() } } }) }) Hooks.on("getCombatTrackerEntryContext", (html, options) => { LethalFantasyUtils.pushCombatOptions(html, options); }); // Hook pour ajouter les données d'attaque au message de défense Hooks.on("preCreateChatMessage", (message) => { const rollType = message.rolls[0]?.options?.rollType // Si c'est un message de défense et qu'on a des données en attente if ((rollType === "weapon-defense" || rollType === "monster-defense") && game.lethalFantasy?.nextDefenseData) { // Ajouter les données dans les flags du message message.updateSource({ [`flags.${SYSTEM.id}.attackData`]: game.lethalFantasy.nextDefenseData }) console.log("Added attack data to defense message:", game.lethalFantasy.nextDefenseData) // Nettoyer delete game.lethalFantasy.nextDefenseData } }) // Hook global pour gérer l'offre de Grit à l'attaquant après une défense Hooks.on("createChatMessage", async (message) => { const rollType = message.rolls[0]?.options?.rollType console.log("Defense hook checking message, rollType:", rollType) // Vérifier si c'est un message de défense if (rollType !== "weapon-defense" && rollType !== "monster-defense") return // Récupérer les données d'attaque depuis les flags const attackData = message.flags?.[SYSTEM.id]?.attackData console.log("Defense message confirmed, attackData:", attackData) if (!attackData) { console.log("No attack data found in message flags") return } const { attackerId, attackRoll, attackerName, defenderName, attackWeaponId, attackRollType, attackRollKey, defenderId } = attackData let defenseRoll = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0 console.log("Processing defense:", { attackRoll, defenseRoll, attackerId, defenderId }) // Attendre l'animation 3D if (game?.dice3d) { await game.dice3d.waitFor3DAnimationByMessageID(message.id) } // Récupérer le défenseur et l'attaquant const defender = game.actors.get(defenderId) const attacker = game.actors.get(attackerId) // Si le défenseur est un personnage qui perd, proposer Grit/Luck (seulement s'il a des points) // Seulement si l'utilisateur actuel est le propriétaire du défenseur let defenderHandledBonus = false if (defender?.type === "character" && defenseRoll < attackRoll && !game.user.isGM && defender.isOwner) { const hasGritOrLuck = (defender.system.grit.current > 0) || (defender.system.luck.current > 0) if (hasGritOrLuck) { const bonusRoll = await LethalFantasyUtils.offerGritLuckBonus( defender, attackRoll, defenseRoll, attackerName, defenderName ) if (bonusRoll > 0) { defenseRoll += bonusRoll } } defenderHandledBonus = true } let attackRollFinal = attackRoll let attackerHandledBonus = false // Si l'attaquant est un personnage qui perd et a du Grit // Seulement si l'utilisateur actuel est le propriétaire de l'attaquant (pas le MJ) if (attacker?.type === "character" && attackRollFinal <= defenseRoll && attacker.system.grit.current > 0) { // Vérifier si l'utilisateur est un propriétaire non-GM de l'attaquant const isAttackerOwner = !game.user.isGM && attacker.testUserPermission(game.user, "OWNER") if (isAttackerOwner) { console.log("Offering Grit to attacker") const attackBonus = await LethalFantasyUtils.offerAttackerGritBonus( attacker, attackRollFinal, defenseRoll, attackerName, defenderName ) attackRollFinal += attackBonus attackerHandledBonus = true } else { console.log("Not attacker owner or is GM, skipping Grit offer") } } // Créer le message de comparaison - uniquement par le client qui a géré le dernier bonus // Priorité: attaquant si il a géré le bonus, sinon défenseur si il a géré le bonus, sinon défenseur const shouldCreateMessage = attackerHandledBonus || (!attackerHandledBonus && defenderHandledBonus) || (!attackerHandledBonus && !defenderHandledBonus && defender.isOwner) if (shouldCreateMessage) { console.log("Creating comparison message", { attackerHandledBonus, defenderHandledBonus, isDefenderOwner: defender.isOwner }) await LethalFantasyUtils.compareAttackDefense({ attackerName, attackerId, attackRoll: attackRollFinal, attackWeaponId, attackRollType, attackRollKey, defenderName, defenderId, defenseRoll }) } else { console.log("Skipping message creation", { attackerHandledBonus, defenderHandledBonus }) } }) // Hook pour appliquer automatiquement les dégâts si une cible est définie Hooks.on("createChatMessage", async (message) => { // Vérifier si c'est un message de dégâts avec un defenderId const defenderId = message.rolls[0]?.options?.defenderId const isDamage = message.rolls[0]?.options?.rollData?.isDamage console.log("Auto-damage hook:", { defenderId, isDamage, rollType: message.rolls[0]?.options?.rollType }) if (!defenderId || !isDamage) return // Récupérer l'attaquant depuis le roll const attackerId = message.rolls[0]?.options?.actorId const attacker = attackerId ? game.actors.get(attackerId) : null // Déterminer qui doit appliquer les dégâts : // 1. Si l'attaquant a un propriétaire joueur, seul ce joueur applique // 2. Si l'attaquant n'a que le MJ comme propriétaire (monstre), seul le MJ applique const attackerOwners = attacker ? game.users.filter(u => !u.isGM && attacker.testUserPermission(u, "OWNER") ) : [] let shouldApplyDamage = false if (attackerOwners.length > 0) { // L'attaquant a des propriétaires joueurs, seul le premier propriétaire applique shouldApplyDamage = attackerOwners[0].id === game.user.id } else { // L'attaquant n'a que le MJ, seul le MJ applique shouldApplyDamage = game.user.isGM } if (!shouldApplyDamage) { console.log("Auto-damage hook: Not responsible for applying damage, skipping") return } console.log("Auto-damage hook: Applying damage as responsible user") // Attendre l'animation 3D avant d'appliquer les dégâts if (game?.dice3d) { await game.dice3d.waitFor3DAnimationByMessageID(message.id) } // Récupérer le défenseur const defender = game.actors.get(defenderId) if (!defender) { console.warn("Defender not found:", defenderId) return } // Récupérer les dégâts (utiliser rollTotal qui contient le total calculé) const damageTotal = message.rolls[0]?.options?.rollTotal || message.rolls[0]?.total || 0 const weaponName = message.rolls[0]?.options?.rollName || "Unknown Weapon" const attackerName = message.rolls[0]?.options?.actorName || "Unknown Attacker" // 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 const messageContent = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-lethal-fantasy/templates/damage-applied-message.hbs", { targetName: defender.name, damage: finalDamage, drText: armorDR > 0 ? `Armor DR: ${armorDR}` : "", weaponName: weaponName, attackerName: attackerName, rawDamage: damageTotal } ) await ChatMessage.create({ content: messageContent, speaker: ChatMessage.getSpeaker({ actor: defender }) }) }) /** * Create a macro when dropping an entity on the hotbar * Item - open roll dialog * Actor - open actor sheet * Journal - open journal sheet */ Hooks.on("hotbarDrop", (bar, data, slot) => { if (["Actor", "Item", "JournalEntry", "roll", "rollDamage", "rollAttack"].includes(data.type)) { Macros.createLethalFantasyMacro(data, slot); return false } }) /** * Register world usage statistics * @param {string} registerKey */ async function registerWorldCount(registerKey) { if (game.user.isGM) { try { ClassCounter.registerUsageCount(game.system.id, {}) } catch { console.log("No usage log ") } } }