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)
This commit is contained in:
@@ -13,3 +13,4 @@ export { default as HamalronTarotSheet } from "./sheets/tarot-sheet.mjs"
|
||||
export { default as HamalronItemSheet } from "./sheets/base-item-sheet.mjs"
|
||||
export { default as HamalronActorSheet } from "./sheets/base-actor-sheet.mjs"
|
||||
export { default as HamalronTarotDeckManager } from "./tarot-deck-manager.mjs"
|
||||
export { default as HamalronSortilegeCasting } from "./sortilege-casting.mjs"
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class HamalronPersonnageSheet extends HamalronActorSheet {
|
||||
modifyResistance: HamalronPersonnageSheet.#onModifyResistance,
|
||||
discardCard: HamalronPersonnageSheet.#onDiscardCard,
|
||||
rollCompetence: HamalronPersonnageSheet.#onRollCompetence,
|
||||
castSortilege: HamalronPersonnageSheet.#onCastSortilege,
|
||||
drawCard: HamalronPersonnageSheet.#onDrawCard,
|
||||
selectCard: HamalronPersonnageSheet.#onSelectCard,
|
||||
discardAndDraw: HamalronPersonnageSheet.#onDiscardAndDraw,
|
||||
@@ -345,6 +346,17 @@ export default class HamalronPersonnageSheet extends HamalronActorSheet {
|
||||
}
|
||||
}
|
||||
|
||||
static async #onCastSortilege(event, target) {
|
||||
const itemId = $(target).data("item-id")
|
||||
const item = this.document.items.get(itemId)
|
||||
if (!item || item.type !== "sortilege") {
|
||||
console.warn(`HamalronPersonnageSheet: Sortilege with ID ${itemId} not found`)
|
||||
return
|
||||
}
|
||||
const { default: HamalronSortilegeCasting } = await import("../sortilege-casting.mjs")
|
||||
await HamalronSortilegeCasting.prompt(item, this.document)
|
||||
}
|
||||
|
||||
static async #onRollCompetence(event, target) {
|
||||
const itemId = $(target).data("item-id")
|
||||
const item = this.document.items.get(itemId)
|
||||
|
||||
@@ -23,7 +23,9 @@ export default class HamalronSortilegeSheet extends HamalronItemSheet {
|
||||
async _prepareContext() {
|
||||
const context = await super._prepareContext()
|
||||
context.enrichedDescription = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.description, { async: true })
|
||||
|
||||
context.enrichedEffetAmateur = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.effetAmateur, { async: true })
|
||||
context.enrichedEffetCompetent = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.effetCompetent, { async: true })
|
||||
context.enrichedEffetExpert = await foundry.applications.ux.TextEditor.implementation.enrichHTML(this.document.system.effetExpert, { async: true })
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,290 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,21 @@ export const ENEMY_TYPES = {
|
||||
}
|
||||
|
||||
export const SORTILEGE_CATEGORIES = {
|
||||
"malefica": { id: "malefica", label: "Maléfica" },
|
||||
"ritual": { id: "ritual", label: "Rituel" },
|
||||
"domestique": { id: "domestique", label: "Domestique" },
|
||||
"specialise": { id: "specialise", label: "Spécialisé" },
|
||||
"miraculeux": { id: "miraculeux", label: "Miraculeux" },
|
||||
}
|
||||
|
||||
export const SORTILEGE_MODIFICATEURS = {
|
||||
amateur: { domestique: 2, specialise: 6, miraculeux: 9 },
|
||||
competent: { domestique: 0, specialise: 4, miraculeux: 7 },
|
||||
expert: { domestique: -2, specialise: 2, miraculeux: 5 },
|
||||
}
|
||||
|
||||
export const RANG_MAGICIEN = {
|
||||
ensorceleur: { id: "ensorceleur", label: "Ensorceleur" },
|
||||
initie: { id: "initie", label: "Initié" },
|
||||
thaumaturge: { id: "thaumaturge", label: "Thaumaturge" },
|
||||
}
|
||||
|
||||
export const NIVEAU_COMPETENCES = {
|
||||
@@ -141,6 +154,8 @@ export const SYSTEM = {
|
||||
COMPETENCE_TYPES: COMPETENCE_TYPES,
|
||||
ENEMY_TYPES: ENEMY_TYPES,
|
||||
SORTILEGE_CATEGORIES: SORTILEGE_CATEGORIES,
|
||||
SORTILEGE_MODIFICATEURS: SORTILEGE_MODIFICATEURS,
|
||||
RANG_MAGICIEN: RANG_MAGICIEN,
|
||||
NIVEAU_COMPETENCES: NIVEAU_COMPETENCES,
|
||||
CHOICE_ADVANTAGES_DISADVANTAGES,
|
||||
ASCII
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class HamalronPersonnage extends foundry.abstract.TypeDataModel {
|
||||
|
||||
schema.historial = new fields.HTMLField({ required: true, textSearch: true })
|
||||
schema.progression = new fields.NumberField({ ...requiredInteger, initial: 0, min: 0 })
|
||||
schema.rangMagie = new fields.StringField({ required: true, initial: "initie", choices: Object.fromEntries(Object.entries(SYSTEM.RANG_MAGICIEN).map(([key, val]) => [key, val.label])) })
|
||||
|
||||
schema.biodata = new fields.SchemaField({
|
||||
age: new fields.StringField({ required: true, nullable: false, initial: "" }),
|
||||
|
||||
@@ -4,15 +4,20 @@ export default class HamalronSortilege extends foundry.abstract.TypeDataModel {
|
||||
static defineSchema() {
|
||||
const fields = foundry.data.fields
|
||||
const schema = {}
|
||||
const requiredInteger = { required: true, nullable: false, integer: true }
|
||||
|
||||
schema.description = new fields.HTMLField({ required: true, textSearch: true })
|
||||
|
||||
schema.categorie = new fields.StringField({ required: true, initial: "malefica", choices: Object.fromEntries(Object.entries(SYSTEM.SORTILEGE_CATEGORIES).map(([key, val]) => [key, val.label])) })
|
||||
schema.categorie = new fields.StringField({ required: true, initial: "domestique", choices: Object.fromEntries(Object.entries(SYSTEM.SORTILEGE_CATEGORIES).map(([key, val]) => [key, val.label])) })
|
||||
schema.niveauRequis = new fields.StringField({ required: true, initial: "amateur", choices: Object.fromEntries(Object.entries(SYSTEM.NIVEAU_COMPETENCES).map(([key, val]) => [key, val.name])) })
|
||||
schema.portee = new fields.StringField({ required: false, initial: "" })
|
||||
schema.duree = new fields.StringField({ required: false, initial: "" })
|
||||
schema.conditions = new fields.StringField({ required: false, initial: "" })
|
||||
schema.effetAmateur = new fields.HTMLField({ required: false, textSearch: true })
|
||||
schema.effetCompetent = new fields.HTMLField({ required: false, textSearch: true })
|
||||
schema.effetExpert = new fields.HTMLField({ required: false, textSearch: true })
|
||||
schema.concentration = new fields.BooleanField({ required: true, initial: false })
|
||||
return schema
|
||||
}
|
||||
|
||||
/** @override */
|
||||
static LOCALIZATION_PREFIXES = ["HAMALRON.Sortilege"]
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user