import { SYSTEM } from "../config/system.mjs" import HamalronUtils from "../utils.mjs" export default class HamalronTirageTarot extends Roll { /** * The HTML template path used to render dice checks of this type * @type {string} */ static CHAT_TEMPLATE = "systems/fvtt-hamalron/templates/chat-message.hbs" /** * Handle Le Mat special effect when distributed to a player * @param {Actor} actor - The actor who received Le Mat * @param {HamalronTarotDeckManager} deckManager - The deck manager instance */ static async handleLeMatEffect(actor, deckManager) { // 1. Défausser toutes les cartes du joueur const allTarotCards = actor.items.filter(i => i.type === "tarot") if (allTarotCards.length > 0) { const cardIds = allTarotCards.map(c => c.id) await actor.deleteEmbeddedDocuments("Item", cardIds) console.log(`Discarded ${cardIds.length} cards from player`) // Ajouter toutes les cartes à la défausse for (const card of allTarotCards) { deckManager.addToDiscard({ id: card.id, name: card.name, img: card.img, symbole: card.system.symbole, valeur: card.system.valeur, type: card.system.type, description: card.system.description, }) } } // 2. Tirer 7 nouvelles cartes pour le joueur for (let i = 0; i < 7; i++) { if (deckManager.deck.length > 0) { const newCards = deckManager.drawCards(1) if (newCards.length > 0) { const newCard = newCards[0] const originalItem = await HamalronUtils.findTarotItem(newCard) if (originalItem) { await actor.createEmbeddedDocuments("Item", [originalItem.toObject()]) } } } } // 3. Regrouper la défausse et le deck, puis mélanger await deckManager.resetDeck() deckManager.render() // 4. Proposer aux autres joueurs de renouveler leur main if (game.user.isGM) { const refreshOthers = await foundry.applications.api.DialogV2.confirm({ window: { title: "Le Mat - Renouvellement" }, content: `

Le Mat a été tiré ! Les autres joueurs peuvent renouveler leur main s'ils le désirent.

`, rejectClose: false, modal: true, }) if (refreshOthers) { for (const otherActor of game.actors.filter(a => a.type === "personnage")) { if (otherActor.id === actor.id) continue const otherCards = otherActor.items.filter(i => i.type === "tarot") if (otherCards.length > 0) { const otherCardIds = otherCards.map(c => c.id) await otherActor.deleteEmbeddedDocuments("Item", otherCardIds) for (let i = 0; i < 7; i++) { if (deckManager.deck.length > 0) { const newCards = deckManager.drawCards(1) if (newCards.length > 0) { const originalItem = await HamalronUtils.findTarotItem(newCards[0]) if (originalItem) { await otherActor.createEmbeddedDocuments("Item", [originalItem.toObject()]) } } } } } } await deckManager.saveDeckState() deckManager.render() } } ui.notifications.info(`Le Mat ! Toutes vos cartes ont été défaussées et vous recevez 7 nouvelles cartes. Le deck a été mélangé.`) } static async prompt(options = {}) { let formula = `3D6 + 0D6KH - 0D6KH + ${options?.rollItem?.value}` let actor = game.actors.get(options.actorId) switch (options.rollType) { case "stat": break case "competence": { // Récupérer les cartes de tarot du personnage options.tarotCards = actor.items.filter(i => i.type === "tarot") // Récupérer le modificateur de compétence depuis NIVEAU_COMPETENCES const niveauData = SYSTEM.NIVEAU_COMPETENCES[options.rollItem.system.niveau] options.competenceModifier = niveauData ? niveauData.modificateur : 0 if (actor.system.malusBlessure) { options.competenceModifier += actor.system.malusBlessure } // Marquer les cartes de succès if (options.tarotCards && actor.system.cartesSucces) { const competenceSymbole = options.rollItem.system.carteSymbole options.tarotCards = options.tarotCards.map(card => { const isSuccessCard = card.system.type !== "atout" && card.system.symbole === competenceSymbole && actor.system.cartesSucces[competenceSymbole] && String(card.system.valeur) === String(actor.system.cartesSucces[competenceSymbole].value) // Créer un objet simple avec toutes les propriétés nécessaires return { id: card.id, name: card.name, img: card.img, system: card.system, isSuccessCard: isSuccessCard } }) } } break case "damage": { let formula = options.rollItem.system.damage if (options.rollItem?.system?.damageStat && options.rollItem.system.damageStat !== "none" && options.rollItem.system.damageStat !== "") { let statKey = options.rollItem.system?.damageStat.toLowerCase() let statValue = actor.system?.stats?.[statKey]?.value if (statValue != null) { formula = `${formula} + ${statValue}` } } let damageRoll = new Roll(formula) await damageRoll.evaluate() await damageRoll.toMessage({ flavor: `

${options.rollItem.name}


${options.rollItem.system.damageType}
`, }); return } case "weapon": { options.weapon = foundry.utils.duplicate(options.rollItem) let statKey = options.weapon.system?.stat?.toLowerCase() if (statKey && actor.system?.stats?.[statKey]) { options.rollItem = actor.system.stats[statKey] } } break default: break } const rollModes = foundry.utils.duplicate(CONFIG.Dice.rollModes) const fieldRollMode = new foundry.data.fields.StringField({ choices: rollModes, blank: false, default: "public", }) options.formula = formula options.nbAdvantages = "0" options.nbDisadvantages = "0" let dialogContext = { actorId: options.actorId, actorName: options.actorName, actorImage: options.actorImage, rollType: options.rollType, rollItem: foundry.utils.duplicate(options.rollItem), // Object only, no class weapon: options?.weapon, formula: formula, fullFormula: formula, rollModes, fieldRollMode, choiceAdvantages: SYSTEM.CHOICE_ADVANTAGES_DISADVANTAGES, choiceDisadvantages: SYSTEM.CHOICE_ADVANTAGES_DISADVANTAGES, hasTarget: options.hasTarget, difficulty: "0", nbAdvantages: "0", nbDisadvantages: "0", tarotCards: options.tarotCards || [], competenceModifier: options.competenceModifier || 0, difficultyChoices: Object.fromEntries(Object.entries(SYSTEM.DIFFICULTY_CHOICES).map(([key, val]) => [key, val.label])), } const content = await foundry.applications.handlebars.renderTemplate("systems/fvtt-hamalron/templates/roll-dialog.hbs", dialogContext) const title = HamalronTirageTarot.createTitle(options.rollType, options.rollTarget) const label = game.i18n.localize("HAMALRON.Roll.roll") const cancelLabel = game.i18n.localize("HAMALRON.Roll.cancel") const rollContext = await foundry.applications.api.DialogV2.wait({ window: { title: title }, classes: ["fvtt-hamalron"], position: { width: 420 }, content, buttons: [ { action: "cancel", label: cancelLabel, default: false, }, { action: "roll", label: label, default: true, disabled: options.rollType === "competence", callback: (event, button, dialog) => { const output = Array.from(button.form.elements).reduce((obj, input) => { if (input.name) obj[input.name] = input.value return obj }, {}) // Ajouter les données de sélection de carte pour les compétences if (options.rollType === "competence") { console.log("DEBUG CALLBACK: options.selectedCardId =", options.selectedCardId, "options.selectedCardValue =", options.selectedCardValue) output.selectedCardId = options.selectedCardId output.selectedCardValue = options.selectedCardValue output.selectedDifficulty = options.selectedDifficulty output.targetScore = options.targetScore console.log("DEBUG CALLBACK: output =", output) } return output }, }, ], actions: { }, rejectClose: false, // Click on Close button will not launch an error render: (event, dialog) => { // Gestion de la sélection de carte pour les compétences if (options.rollType === "competence") { let selectedCardId = null options.selectedDifficulty = "soustension_oppose" options.targetScore = null // Calculer et afficher les valeurs pour chaque carte const competenceSymbole = options.rollItem.system.carteSymbole $(".tarot-card-select").each(function () { const cardSymbole = $(this).data("card-symbole") const cardValeur = $(this).data("card-valeur") const cardType = $(this).data("card-type") const valeurData = SYSTEM.TAROT_VALEURS[cardValeur] const baseValue = valeurData ? valeurData.value : 0 let calculatedValue = 0 if (cardType === "atout") { calculatedValue = baseValue } else if (cardSymbole === competenceSymbole) { calculatedValue = baseValue } else { calculatedValue = Math.ceil(baseValue / 2) } $(this).find(".card-values").text(`${baseValue} → ${calculatedValue}`) }) // Gestion du changement de difficulté $(".difficulty-select").change(function () { const difficultyKey = $(this).val() options.selectedDifficulty = difficultyKey const difficultyData = SYSTEM.DIFFICULTY_CHOICES[difficultyKey] if (difficultyData.standard === null) { options.targetScore = null $("#target-score").text("-") } else { options.targetScore = difficultyData.standard $("#target-score").text(options.targetScore) } }) // Tirage d'une carte pour la difficulté (épreuve sous tension) $("#draw-difficulty-btn").click(async function () { const deckManager = globalThis.HamalronTarotDeckManager?._instance if (!deckManager || deckManager.deck.length === 0) { ui.notifications.warn(game.i18n.localize("HAMALRON.Notifications.deckEmpty")) return } const drawn = deckManager.drawCards(1) if (drawn.length > 0) { const card = drawn[0] const valData = SYSTEM.TAROT_VALEURS[card.valeur] const val = valData?.value ?? 0 options.targetScore = val options.selectedDifficulty = "soustension_oppose" $(".difficulty-select").val("soustension_oppose") $("#target-score").text(String(val)) $("#drawn-card-name").text(`${card.name} (${val})`) $("#drawn-card-info").show() deckManager.addToDiscard(card) await deckManager.saveDeckState() } }) $(".tarot-card-select").click(function () { // Désélectionner toutes les cartes $(".tarot-card-select").removeClass("selected") // Sélectionner la carte cliquée $(this).addClass("selected") selectedCardId = $(this).data("card-id") console.log("DEBUG CLICK: selectedCardId =", selectedCardId) const cardSymbole = $(this).data("card-symbole") const cardValeur = $(this).data("card-valeur") const cardType = $(this).data("card-type") // Calculer la valeur de la carte const competenceSymbole = options.rollItem.system.carteSymbole let cardValue = 0 // Récupérer la valeur numérique de la carte depuis TAROT_VALEURS const valeurData = SYSTEM.TAROT_VALEURS[cardValeur] const baseValue = valeurData ? valeurData.value : 0 if (cardType === "atout") { // Si c'est un atout, prendre directement la valeur cardValue = baseValue } else if (cardSymbole === competenceSymbole) { // Si le symbole correspond, prendre la valeur complète cardValue = baseValue } else { // Sinon, diviser par 2 arrondi au supérieur cardValue = Math.ceil(baseValue / 2) } // Stocker la carte sélectionnée et sa valeur calculée options.selectedCardId = selectedCardId options.selectedCardValue = cardValue console.log("DEBUG CLICK END: options.selectedCardId =", options.selectedCardId, "options.selectedCardValue =", options.selectedCardValue) // Mettre à jour l'affichage $("#card-value-display").text(`+ ${cardValue}`) $("#total-value").text(options.competenceModifier + cardValue) // Activer le bouton de lancement $("button[data-action='roll']").prop("disabled", false) }) // Permettre de désélectionner en cliquant à nouveau $(".tarot-card-select").dblclick(function () { $(this).removeClass("selected") options.selectedCardId = null options.selectedCardValue = 0 $("#card-value-display").text("+ 0") $("#total-value").text(options.competenceModifier) // Désactiver le bouton de lancement $("button[data-action='roll']").prop("disabled", true) }) } } }) // If the user cancels the dialog, exit if (rollContext === null) return // Fusionner les données du dialogue dans options Object.assign(options, rollContext) // Gestion spéciale pour les compétences (pas de dés) if (options.rollType === "competence") { const totalValue = options.competenceModifier + (options.selectedCardValue || 0) // Récupérer la carte sélectionnée si elle existe let selectedCard = null let cardSymboleMatch = false let isAutomaticSuccess = false if (options.selectedCardId) { selectedCard = actor.items.get(options.selectedCardId) if (selectedCard) { cardSymboleMatch = selectedCard.system.symbole === options.rollItem.system.carteSymbole if (actor.system.cartesSucces && selectedCard.system.type !== "atout") { const competenceSymbole = options.rollItem.system.carteSymbole isAutomaticSuccess = selectedCard.system.symbole === competenceSymbole && actor.system.cartesSucces[competenceSymbole] && String(selectedCard.system.valeur) === String(actor.system.cartesSucces[competenceSymbole].value) } } } // Préparer les données pour le chat const difficultyData = SYSTEM.DIFFICULTY_CHOICES[options.selectedDifficulty || "moyenne"] const isSousTensionOppose = options.selectedDifficulty === "soustension_oppose" const isSuccess = isAutomaticSuccess || totalValue >= options.targetScore // Détecter si la carte est un valet const isValet = selectedCard?.system.valeur === "valet" // Détecter si la carte est un cavalier const isCavalier = selectedCard?.system.valeur === "cavalier" // Détecter si la carte est une reine const isReine = selectedCard?.system.valeur === "reine" // Détecter si la carte est un roi const isRoi = selectedCard?.system.valeur === "roi" // Détecter si la carte est un atout const isAtout = selectedCard?.system.type === "atout" // Si c'est un cavalier et un échec, transformer en égalité const isEquality = isCavalier && !isSuccess && !isSousTensionOppose const chatData = { cssClass: [SYSTEM.id, "competence-roll"].join(" "), actingCharImg: options.actorImage, actingCharName: options.actorName, actorName: options.actorName, competence: options.rollItem, competenceModifier: options.competenceModifier, selectedCard: selectedCard, cardBonus: options.selectedCardValue || 0, cardSymboleMatch: cardSymboleMatch, totalValue: totalValue, difficulty: difficultyData.label, targetScore: options.targetScore, isSuccess: isSuccess, isAutomaticSuccess: isAutomaticSuccess, isSousTensionOppose: isSousTensionOppose, isValet: isValet, isCavalier: isCavalier, isReine: isReine, isRoi: isRoi, isAtout: isAtout, isEquality: isEquality, } // Créer le message de chat const content = await foundry.applications.handlebars.renderTemplate( "systems/fvtt-hamalron/templates/chat-competence.hbs", chatData ) const messageData = { user: game.user.id, speaker: ChatMessage.getSpeaker({ actor }), content: content, style: CONST.CHAT_MESSAGE_STYLES.OTHER, } // Appliquer le mode de visibilité ChatMessage.applyRollMode(messageData, rollContext.visibility) await ChatMessage.create(messageData) // Gérer la carte utilisée console.log("DEBUG: selectedCard =", selectedCard, "options.selectedCardId =", options.selectedCardId) if (selectedCard) { console.log("Managing used card:", selectedCard.name) // Supprimer la carte de l'acteur const cardItem = actor.items.get(selectedCard.id) if (cardItem) { await actor.deleteEmbeddedDocuments("Item", [selectedCard.id]) console.log("Card deleted from actor") } // Récupérer le deck manager singleton const module = await import("../applications/tarot-deck-manager.mjs") const HamalronTarotDeckManager = module.default let deckManager = HamalronTarotDeckManager._instance // Si le deck manager n'existe pas, le créer if (!deckManager) { console.log("Deck manager not initialized, creating new instance...") deckManager = new HamalronTarotDeckManager() await deckManager.resetDeck() } console.log("Deck manager instance:", deckManager) if (deckManager) { console.log("Deck manager is available") // Ajouter la carte à la défausse deckManager.addToDiscard({ id: selectedCard.id, name: selectedCard.name, img: selectedCard.img, symbole: selectedCard.system.symbole, valeur: selectedCard.system.valeur, type: selectedCard.system.type, description: selectedCard.system.description, }) console.log("Card added to discard") // Tirer une nouvelle carte automatiquement console.log("Attempting to draw new card. Deck length:", deckManager.deck.length) if (deckManager.deck.length > 0) { const newCards = deckManager.drawCards(1) console.log("Cards drawn:", newCards) if (newCards.length > 0) { const newCard = newCards[0] console.log("New card to add:", newCard) const originalItem = await HamalronUtils.findTarotItem(newCard) if (originalItem) { // Cas spécial : Le Mat if (newCard.valeur === "lemat") { console.log("Le Mat drawn! Executing special rules...") await HamalronTirageTarot.handleLeMatEffect(actor, deckManager) } else { console.log("Creating embedded document for actor...") await actor.createEmbeddedDocuments("Item", [originalItem.toObject()]) console.log("New card added to actor") ui.notifications.info(`${game.i18n.localize("HAMALRON.Notifications.newCardDrawn")}: ${newCard.name}`) } } else { console.warn("Original item not found for card:", newCard) } } else { console.log("No cards were drawn (empty result)") } } else { console.log("Deck is empty, cannot draw new card") } await deckManager.saveDeckState() deckManager.render() ui.notifications.info(`${game.i18n.localize("HAMALRON.Notifications.cardUsed")}: ${selectedCard.name}`) } } return null } // Code existant pour les autres types de jets let rollData = foundry.utils.mergeObject(foundry.utils.duplicate(options), rollContext) rollData.rollMode = rollContext.visibility rollData.targetScore = 8 if (Hooks.call("fvtt-hamalron.preRoll", options, rollData) === false) return options.nbAdvantages = Number(options.nbAdvantages) options.nbDisadvantages = Number(options.nbDisadvantages) let dice = 3; let keep = "" if ((options.nbAdvantages !== options.nbDisadvantages) && options.nbAdvantages > 0 || options.nbDisadvantages > 0) { dice = 3 + Math.abs(options.nbAdvantages - options.nbDisadvantages) if (options.nbAdvantages > options.nbDisadvantages) { keep = "kh3" } else if (options.nbAdvantages < options.nbDisadvantages) { keep = "kl3" } } let diceFormula = `${dice}D6${keep} + ${options.rollItem.value}` const roll = new this(diceFormula, options.data, rollData) await roll.evaluate() console.log("Roll", rollData, roll) options.difficulty = (rollData.difficulty === "unknown") ? 0 : (Number(rollData.difficulty) || 0) roll.displayRollResult(roll, options, rollData, roll) if (Hooks.call("fvtt-hamalron.Roll", options, rollData, roll) === false) return return roll } displayRollResult(formula, options, rollData, roll) { let resultType = "failure" if (options.difficulty === 0) { resultType = "unknown" } else if (this.total >= options.difficulty) { resultType = "success" } // Compute the result quality this.options.satanicSuccess = false this.options.fiendishFailure = false this.options.rollData = foundry.utils.duplicate(rollData) // Check if all results are equal let workResults = foundry.utils.duplicate(roll.terms[0].results) // Get the most common result of the roll let commonResult = workResults.reduce((acc, r) => { acc[r.result] = (acc[r.result] || 0) + 1 return acc }, {}) commonResult = Object.entries(commonResult).reduce((a, b) => (a[1] > b[1]) ? a : b)[0] let nbEqual = workResults.filter(r => Number(r.result) === Number(commonResult)).length if (resultType === "success" && commonResult >= 4 && nbEqual >= 3) { this.options.satanicSuccess = true if (this.options.satanicSuccess) { resultType = "success" } } if (resultType === "failure" && commonResult <= 3 && nbEqual >= 3) { this.options.fiendishFailure = true if (this.options.fiendishFailure) { resultType = "failure" } } this.options.resultType = resultType this.options.isSuccess = resultType === "success" this.options.isFailure = resultType === "failure" this.options.results = roll.terms[0].results } /** * Creates a title based on the given type. * * @param {string} type The type of the roll. * @param {string} target The target of the roll. * @returns {string} The generated title. */ static createTitle(type, target) { switch (type) { case "stat": return `${game.i18n.localize("HAMALRON.Label.titleStat")}` case "weapon": return `${game.i18n.localize("HAMALRON.Label.titleWeapon")}` case "competence": return `${game.i18n.localize("HAMALRON.Label.titleCompetence")}` default: return game.i18n.localize("HAMALRON.Label.titleStandard") } } /** @override */ async render(chatOptions = {}) { let chatData = await this._getChatCardData(chatOptions.isPrivate) return await foundry.applications.handlebars.renderTemplate(this.constructor.CHAT_TEMPLATE, chatData) } /** * Generates the data required for rendering a roll chat card. * * @param {boolean} isPrivate Indicates if the chat card is private. * @returns {Promise} A promise that resolves to an object containing the chat card data. * @property {Array} css - CSS classes for the chat card. * @property {Object} data - The data associated with the roll. * @property {number} diceTotal - The total value of the dice rolled. * @property {boolean} isGM - Indicates if the user is a Game Master. * @property {string} formula - The formula used for the roll. * @property {number} total - The total result of the roll. * @property {boolean} isFailure - Indicates if the roll is a failure. * @property {string} actorId - The ID of the actor performing the roll. * @property {string} actingCharName - The name of the character performing the roll. * @property {string} actingCharImg - The image of the character performing the roll. * @property {string} resultType - The type of result (e.g., success, failure). * @property {boolean} hasTarget - Indicates if the roll has a target. * @property {string} targetName - The name of the target. * @property {number} targetArmor - The armor value of the target. * @property {number} realDamage - The real damage dealt. * @property {boolean} isPrivate - Indicates if the chat card is private. * @property {string} cssClass - The combined CSS classes as a single string. * @property {string} tooltip - The tooltip text for the chat card. */ async _getChatCardData(isPrivate) { let cardData = foundry.utils.duplicate(this.options) cardData.css = [SYSTEM.id, "dice-roll"] cardData.data = this.data cardData.diceTotal = this.dice.reduce((t, d) => t + d.total, 0) cardData.isGM = game.user.isGM cardData.formula = this.formula cardData.fullFormula = this.options.fullFormula cardData.numericModifier = this.options.numericModifier cardData.total = this.total cardData.actorId = this.actorId cardData.actingCharName = this.actorName cardData.actingCharImg = this.actorImage cardData.resultType = this.resultType cardData.hasTarget = this.hasTarget cardData.targetName = this.targetName cardData.targetArmor = this.targetArmor cardData.realDamage = this.realDamage cardData.isPrivate = isPrivate cardData.weapon = this.weapon cardData.cssClass = cardData.css.join(" ") cardData.tooltip = isPrivate ? "" : await this.getTooltip() return cardData } /** * Converts the roll result to a chat message. * * @param {Object} [messageData={}] Additional data to include in the message. * @param {Object} options Options for message creation. * @param {string} options.rollMode The mode of the roll (e.g., public, private). * @param {boolean} [options.create=true] Whether to create the message. * @returns {Promise} - A promise that resolves when the message is created. */ async toMessage(messageData = {}, { rollMode, create = true } = {}) { return super.toMessage( { isFailure: this.resultType === "failure", actingCharName: this.actorName, actingCharImg: this.actorImage, hasTarget: this.hasTarget, realDamage: this.realDamage, ...messageData, }, { rollMode, create }, ) } }