Files
fvtt-hamalron/module/applications/sortilege-casting.mjs
T
uberwald 1749210322 feat: système de magie complet avec dialogue de lancement
- Modèle sortilege enrichi (type, niveau, portée, durée, conditions, effets)
- Personnage: champ rangMagie (ensorceleur/initié/thaumaturge)
- Constantes: SORTILEGE_MODIFICATEURS (table 3x3), RANG_MAGICIEN
- Dialogue de lancement: tirage MJ, sélection atouts + Coupe, total temps réel
- Calcul difficulté: carte MJ + modificateur selon type × niveau
- Table des échecs magiques (normal/critique selon rang)
- Pioche automatique après succès, défausse après échec
- Template chat avec résultat localisé

Corrections post-review:
- Boucle de défausse sur échec corrigée (itérait toujours sur [0])
- Clé de localisation HAMALRON.Roll.cast -> HAMALRON.Sortilege.cast
- Ajout de la clé manquante noCoupe
- Labels localisés dans le chat (categorieLabel, niveauLabel)
2026-07-10 19:07:05 +02:00

291 lines
11 KiB
JavaScript

import { SYSTEM } from "../config/system.mjs"
import HamalronUtils from "../utils.mjs"
export default class HamalronSortilegeCasting {
static CHAT_TEMPLATE = "systems/fvtt-hamalron/templates/chat-sortilege.hbs"
static DIALOG_TEMPLATE = "systems/fvtt-hamalron/templates/dialog-sortilege-casting.hbs"
static getModificateur(categorie, niveau) {
const mods = SYSTEM.SORTILEGE_MODIFICATEURS[niveau]
return mods?.[categorie] ?? 0
}
static getRangMagie(actor) {
return actor.system.rangMagie || "initie"
}
static getEchecMagique(valeurCarteMJ, rang) {
const rangs = ["ensorceleur", "initie", "thaumaturge"]
const idx = rangs.indexOf(rang)
if (idx === -1) return "normal"
const paliers = [
{ max: 10, resultats: ["normal", "normal", "normal"] },
{ max: 13, resultats: ["critique", "normal", "normal"] },
{ max: 16, resultats: ["critique", "normal", "normal"] },
{ max: 19, resultats: ["critique", "critique", "normal"] },
{ max: 23, resultats: ["critique", "critique", "critique"] },
]
for (const palier of paliers) {
if (valeurCarteMJ <= palier.max) {
return palier.resultats[idx]
}
}
return "critique"
}
static async prompt(spellItem, actor) {
const atouts = actor.items.filter(i => i.type === "tarot" && i.system.type === "atout")
const rang = HamalronSortilegeCasting.getRangMagie(actor)
const peutUtiliserCoupe = rang === "thaumaturge"
const cartesCoupe = peutUtiliserCoupe
? actor.items.filter(i => i.type === "tarot" && i.system.symbole === "coupe" && i.system.type !== "atout")
: []
if (atouts.length === 0) {
ui.notifications.warn(game.i18n.localize("HAMALRON.Sortilege.noAtout"))
return
}
const niveauData = SYSTEM.NIVEAU_COMPETENCES[spellItem.system.niveauRequis]
const niveau = niveauData?.id || spellItem.system.niveauRequis
const modificateur = HamalronSortilegeCasting.getModificateur(spellItem.system.categorie, niveau)
const categorieLabel = SYSTEM.SORTILEGE_CATEGORIES[spellItem.system.categorie]?.label || spellItem.system.categorie
const niveauLabel = niveauData?.name || spellItem.system.niveauRequis
const dialogContext = {
spell: spellItem,
actorName: actor.name,
actorImage: actor.img,
rang: rang,
rangLabel: SYSTEM.RANG_MAGICIEN[rang]?.label || rang,
categorieLabel,
niveauLabel,
modificateur: modificateur >= 0 ? `+${modificateur}` : modificateur,
peutUtiliserCoupe,
atouts: atouts.map(c => ({
id: c.id,
name: c.name,
img: c.img,
valeur: c.system.valeur,
valeurNumerique: HamalronSortilegeCasting.#valeurNumerique(c.system.valeur),
})),
cartesCoupe: cartesCoupe.map(c => ({
id: c.id,
name: c.name,
img: c.img,
valeur: c.system.valeur,
valeurNumerique: HamalronSortilegeCasting.#valeurNumerique(c.system.valeur),
})),
}
const content = await foundry.applications.handlebars.renderTemplate(
HamalronSortilegeCasting.DIALOG_TEMPLATE, dialogContext
)
const result = await foundry.applications.api.DialogV2.wait({
window: { title: `${game.i18n.localize("HAMALRON.Sortilege.cast")}: ${spellItem.name}` },
classes: ["fvtt-hamalron"],
position: { width: 520 },
content,
buttons: [
{
action: "cancel",
label: game.i18n.localize("Cancel"),
default: false,
},
{
action: "cast",
label: game.i18n.localize("HAMALRON.Sortilege.cast"),
default: true,
disabled: true,
callback: (event, button, dialog) => {
const form = button.form.elements
const carteMJ = Number(form["carte-mj"].value) || 0
const selectedAtouts = Array.from(form.querySelectorAll('input[name="atout"]:checked'))
.map(el => atouts.find(c => c.id === el.value))
.filter(Boolean)
const selectedCoupe = Array.from(form.querySelectorAll('input[name="coupe"]:checked'))
.map(el => cartesCoupe.find(c => c.id === el.value))
.filter(Boolean)
return { carteMJ, selectedAtouts, selectedCoupe }
},
},
],
actions: {},
rejectClose: false,
render: (event, dialog) => {
const calcTotal = () => {
const carteMJ = Number(document.getElementById("carte-mj")?.value || 0)
const atoutTotal = Array.from(document.querySelectorAll('input[name="atout"]:checked'))
.reduce((sum, el) => sum + (Number(el.dataset.value) || 0), 0)
const coupeTotal = Array.from(document.querySelectorAll('input[name="coupe"]:checked'))
.reduce((sum, el) => sum + (Number(el.dataset.value) || 0), 0)
const total = atoutTotal + coupeTotal
const difficulte = carteMJ + modificateur
document.getElementById("total-atouts").textContent = String(atoutTotal)
document.getElementById("total-coupe").textContent = String(coupeTotal)
document.getElementById("total-joueur").textContent = String(total)
document.getElementById("difficulte-totale").textContent = String(difficulte)
const castBtn = document.querySelector("button[data-action='cast']")
const peutLancer = atoutTotal > 0 && carteMJ > 0
castBtn.disabled = !peutLancer
}
document.querySelectorAll('input[name="atout"], input[name="coupe"], #carte-mj')
.forEach(el => el.addEventListener("change", calcTotal))
document.getElementById("draw-card-btn")?.addEventListener("click", async () => {
const module = await import("./tarot-deck-manager.mjs")
const HamalronTarotDeckManager = module.default
if (!HamalronTarotDeckManager._instance) {
HamalronTarotDeckManager._instance = new HamalronTarotDeckManager()
}
const cards = HamalronTarotDeckManager._instance.drawCards(1)
if (cards.length > 0) {
const valeurData = SYSTEM.TAROT_VALEURS[cards[0].valeur]
const value = valeurData?.value || 0
document.getElementById("carte-mj").value = String(value)
document.getElementById("carte-mj-nom").textContent = `${cards[0].name} (${value})`
calcTotal()
}
})
calcTotal()
},
})
if (!result) return
if (result.carteMJ === 0 || result.selectedAtouts.length === 0) return
const totalAtouts = result.selectedAtouts.reduce((sum, c) => {
return sum + (HamalronSortilegeCasting.#valeurNumerique(c.system.valeur) || 0)
}, 0)
const totalCoupe = result.selectedCoupe.reduce((sum, c) => {
return sum + (HamalronSortilegeCasting.#valeurNumerique(c.system.valeur) || 0)
}, 0)
const totalJoueur = totalAtouts + totalCoupe
const difficulte = result.carteMJ + modificateur
const isSuccess = totalJoueur >= difficulte
const deckModule = await import("./tarot-deck-manager.mjs")
const HamalronTarotDeckManager = deckModule.default
if (!HamalronTarotDeckManager._instance) {
HamalronTarotDeckManager._instance = new HamalronTarotDeckManager()
}
const deckManager = HamalronTarotDeckManager._instance
if (!isSuccess) {
const echecType = HamalronSortilegeCasting.getEchecMagique(result.carteMJ, rang)
const nbCartesADefausser = Math.max(1, result.selectedAtouts.length)
for (let i = 0; i < nbCartesADefausser && i < result.selectedAtouts.length; i++) {
const cardToDiscard = result.selectedAtouts[i]
deckManager.addToDiscard({
id: cardToDiscard.id,
name: cardToDiscard.name,
img: cardToDiscard.img,
symbole: cardToDiscard.system.symbole,
valeur: cardToDiscard.system.valeur,
type: cardToDiscard.system.type,
})
const cardItem = actor.items.get(cardToDiscard.id)
if (cardItem) {
await actor.deleteEmbeddedDocuments("Item", [cardItem.id])
}
}
const chatDataFail = {
cssClass: [SYSTEM.id, "sortilege-roll"].join(" "),
isSuccess: false,
isNormal: echecType === "normal",
isCritique: echecType === "critique",
actingCharImg: actor.img,
actingCharName: actor.name,
spell: spellItem,
categorieLabel,
niveauLabel,
atoutsUtilises: result.selectedAtouts,
cartesCoupeUtilisees: result.selectedCoupe,
carteMJ: result.carteMJ,
modificateur,
difficulte,
totalJoueur,
}
const failContent = await foundry.applications.handlebars.renderTemplate(
HamalronSortilegeCasting.CHAT_TEMPLATE, chatDataFail
)
await ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor }),
content: failContent,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
})
await deckManager.saveDeckState()
deckManager.render()
return
}
for (const card of [...result.selectedAtouts, ...result.selectedCoupe]) {
deckManager.addToDiscard({
id: card.id,
name: card.name,
img: card.img,
symbole: card.system.symbole,
valeur: card.system.valeur,
type: card.system.type,
})
const cardItem = actor.items.get(card.id)
if (cardItem) {
await actor.deleteEmbeddedDocuments("Item", [card.id])
}
}
const nbCartesAPiocher = result.selectedAtouts.length + result.selectedCoupe.length
for (let i = 0; i < nbCartesAPiocher; i++) {
if (deckManager.deck.length > 0) {
const drawn = deckManager.drawCards(1)
if (drawn.length > 0) {
const originalItem = await HamalronUtils.findTarotItem(drawn[0])
if (originalItem) {
await actor.createEmbeddedDocuments("Item", [originalItem.toObject()])
}
}
}
}
const chatDataSuccess = {
cssClass: [SYSTEM.id, "sortilege-roll"].join(" "),
isSuccess: true,
actingCharImg: actor.img,
actingCharName: actor.name,
spell: spellItem,
categorieLabel,
niveauLabel,
atoutsUtilises: result.selectedAtouts,
cartesCoupeUtilisees: result.selectedCoupe,
carteMJ: result.carteMJ,
modificateur,
difficulte,
totalJoueur,
}
const chatContent = await foundry.applications.handlebars.renderTemplate(
HamalronSortilegeCasting.CHAT_TEMPLATE, chatDataSuccess
)
await ChatMessage.create({
user: game.user.id,
speaker: ChatMessage.getSpeaker({ actor }),
content: chatContent,
type: CONST.CHAT_MESSAGE_TYPES.OTHER,
})
await deckManager.saveDeckState()
deckManager.render()
}
static #valeurNumerique(valeur) {
const data = SYSTEM.TAROT_VALEURS[valeur]
return data?.value ?? (Number.parseInt(valeur) || 0)
}
}