403 lines
14 KiB
JavaScript
403 lines
14 KiB
JavaScript
import { MournbladeUtility } from "../mournblade-utility.js"
|
|
|
|
/**
|
|
* Dialogue de jet de dé pour Mournblade - Version DialogV2
|
|
*/
|
|
export class MournbladeRollDialog {
|
|
|
|
/**
|
|
* Create and display the roll dialog
|
|
* @param {MournbladeActor} actor - The actor making the roll
|
|
* @param {Object} rollData - Data for the roll
|
|
* @returns {Promise<MournbladeRollDialog>}
|
|
*/
|
|
static async create(actor, rollData) {
|
|
// Préparer le contexte pour le template
|
|
const context = {
|
|
...rollData,
|
|
difficulte: String(rollData.difficulte || 0),
|
|
img: actor.img,
|
|
name: actor.name,
|
|
config: game.system.mournblade.config,
|
|
}
|
|
|
|
// Si attrKey est "tochoose", préparer la liste des attributs sélectionnables
|
|
if (rollData.attrKey === "tochoose") {
|
|
context.selectableAttributes = actor.system.attributs
|
|
}
|
|
|
|
// Rendre le template en HTML
|
|
const content = await foundry.applications.handlebars.renderTemplate(
|
|
"systems/fvtt-mournblade/templates/roll-dialog-v2.hbs",
|
|
context
|
|
)
|
|
|
|
// Utiliser DialogV2.wait avec le HTML rendu
|
|
return foundry.applications.api.DialogV2.wait({
|
|
window: { title: "Test de Capacité", icon: "fa-solid fa-dice-d20" },
|
|
classes: ["mournblade-roll-dialog"],
|
|
position: { width: 500 },
|
|
modal: false,
|
|
content,
|
|
buttons: [
|
|
{
|
|
action: "rolld10",
|
|
label: "Lancer 1d10",
|
|
icon: "fa-solid fa-dice-d10",
|
|
default: true,
|
|
callback: async (event, button, dialog) => {
|
|
this._updateRollDataFromForm(rollData, button.form.elements, actor)
|
|
rollData.mainDice = "1d10"
|
|
await MournbladeUtility.rollMournblade(rollData)
|
|
}
|
|
},
|
|
{
|
|
action: "rolld20",
|
|
label: "Lancer 1d20",
|
|
icon: "fa-solid fa-dice-d20",
|
|
callback: async (event, button, dialog) => {
|
|
this._updateRollDataFromForm(rollData, button.form.elements, actor)
|
|
rollData.mainDice = "1d20"
|
|
await MournbladeUtility.rollMournblade(rollData)
|
|
}
|
|
},
|
|
],
|
|
rejectClose: false,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Create and display the sortilège (multi-rune spell) roll dialog
|
|
* @param {MournbladeActor} actor
|
|
* @param {Object} rollData
|
|
*/
|
|
static async createSortilege(actor, rollData) {
|
|
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
|
|
const context = {
|
|
...rollData,
|
|
img: actor.img,
|
|
name: actor.name,
|
|
config: game.system.mournblade.config,
|
|
runes: actor.getRunes(),
|
|
runemode: "prononcer",
|
|
ameDisponible,
|
|
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
|
|
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
|
}
|
|
|
|
const content = await foundry.applications.handlebars.renderTemplate(
|
|
"systems/fvtt-mournblade/templates/dialog-sortilege.hbs",
|
|
context
|
|
)
|
|
|
|
// Attach dynamic recalculation after render
|
|
Hooks.once("renderDialogV2", (_app, html) => {
|
|
const form = html.querySelector ? html : html[0]
|
|
MournbladeRollDialog._attachSortilegeListeners(form)
|
|
})
|
|
|
|
return foundry.applications.api.DialogV2.wait({
|
|
window: { title: "Lancer un Sortilège", icon: "fa-solid fa-star-of-david" },
|
|
classes: ["mournblade-roll-dialog"],
|
|
position: { width: 520 },
|
|
modal: false,
|
|
content,
|
|
buttons: [
|
|
{
|
|
action: "rolld10",
|
|
label: "Lancer 1d10",
|
|
icon: "fa-solid fa-dice-d10",
|
|
default: true,
|
|
callback: async (event, button, dialog) => {
|
|
this._updateSortilegeRollData(rollData, button.form.elements, actor)
|
|
rollData.mainDice = "1d10"
|
|
await MournbladeUtility.rollSortilege(rollData)
|
|
}
|
|
},
|
|
{
|
|
action: "rolld20",
|
|
label: "Lancer 1d20",
|
|
icon: "fa-solid fa-dice-d20",
|
|
callback: async (event, button, dialog) => {
|
|
this._updateSortilegeRollData(rollData, button.form.elements, actor)
|
|
rollData.mainDice = "1d20"
|
|
await MournbladeUtility.rollSortilege(rollData)
|
|
}
|
|
},
|
|
],
|
|
rejectClose: false,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Open the potion preparation dialog
|
|
* @param {MournbladeActor} actor
|
|
* @param {object} rollData
|
|
*/
|
|
static async createPotion(actor, rollData) {
|
|
const ameDisponible = Math.max(1, (actor.system.ame.currentmax - actor.system.ame.value))
|
|
const context = {
|
|
...rollData,
|
|
img: actor.img,
|
|
name: actor.name,
|
|
config: game.system.mournblade.config,
|
|
runes: actor.getRunes(),
|
|
ameDisponible,
|
|
ameOptions: Array.from({ length: ameDisponible }, (_, i) => i + 1),
|
|
modOptions: Array.from({ length: 21 }, (_, i) => i - 10),
|
|
}
|
|
|
|
const content = await foundry.applications.handlebars.renderTemplate(
|
|
"systems/fvtt-mournblade/templates/dialog-potion.hbs",
|
|
context
|
|
)
|
|
|
|
Hooks.once("renderDialogV2", (_app, html) => {
|
|
const form = html.querySelector ? html : html[0]
|
|
MournbladeRollDialog._attachPotionListeners(form)
|
|
})
|
|
|
|
return foundry.applications.api.DialogV2.wait({
|
|
window: { title: "Préparer une Potion", icon: "fa-solid fa-flask" },
|
|
classes: ["mournblade-roll-dialog"],
|
|
position: { width: 520 },
|
|
modal: false,
|
|
content,
|
|
buttons: [
|
|
{
|
|
action: "preparer",
|
|
label: "Préparer",
|
|
icon: "fa-solid fa-flask",
|
|
default: true,
|
|
callback: async (event, button, dialog) => {
|
|
MournbladeRollDialog._updatePotionRollData(rollData, button.form.elements, actor)
|
|
await MournbladeUtility.rollPotion(rollData)
|
|
}
|
|
},
|
|
],
|
|
rejectClose: false,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Attach dynamic recalculation listeners to the potion dialog
|
|
* @param {HTMLElement} html
|
|
*/
|
|
static _attachPotionListeners(html) {
|
|
const recalculate = () => {
|
|
const radioChecked = html.querySelector('.potion-rune-radio:checked')
|
|
const runeRow = radioChecked?.closest('[data-seuil]')
|
|
const seuil = parseInt(runeRow?.dataset.seuil ?? 0)
|
|
const pa = parseInt(html.querySelector('[name="pointsAme"]')?.value ?? 1)
|
|
const mod = parseInt(html.querySelector('[name="modificateur"]')?.value ?? 0)
|
|
|
|
const difficulteEl = html.querySelector('#potion-difficulte')
|
|
if (difficulteEl) difficulteEl.textContent = (seuil + pa) + (mod !== 0 ? ` (mod ${mod > 0 ? '+' : ''}${mod})` : '')
|
|
|
|
const tempsEl = html.querySelector('#potion-temps')
|
|
if (tempsEl) {
|
|
const heures = Math.max(1, Math.ceil(pa / 3))
|
|
tempsEl.textContent = heures === 1 ? '1 heure' : `${heures} heures`
|
|
}
|
|
}
|
|
|
|
html.querySelectorAll('.potion-rune-radio, [name="pointsAme"]').forEach(el => {
|
|
el.addEventListener('change', recalculate)
|
|
})
|
|
recalculate()
|
|
}
|
|
|
|
/**
|
|
* Update rollData from the potion dialog form elements
|
|
* @param {object} rollData
|
|
* @param {HTMLFormControlsCollection} formElements
|
|
* @param {MournbladeActor} actor
|
|
*/
|
|
static _updatePotionRollData(rollData, formElements, actor) {
|
|
const runeId = formElements['rune-selected']?.value
|
|
if (runeId) {
|
|
const rune = actor.getRunes().find(r => r._id === runeId)
|
|
if (rune) {
|
|
rollData.runeId = runeId
|
|
rollData.runeName = rune.name
|
|
rollData.runeImg = rune.img
|
|
rollData.runeSeuil = rune.system.seuil ?? 0
|
|
rollData.runeHautParler = rune.system.hautParler ?? ""
|
|
}
|
|
}
|
|
rollData.pointsAme = parseInt(formElements['pointsAme']?.value ?? 1)
|
|
rollData.forme = formElements['forme']?.value ?? "liquide"
|
|
rollData.modificateur = parseInt(formElements['modificateur']?.value ?? 0)
|
|
}
|
|
|
|
/**
|
|
* Attach dynamic recalculation listeners to the sortilège dialog
|
|
* @param {HTMLElement} html
|
|
*/
|
|
static _attachSortilegeListeners(html) {
|
|
const recalculate = () => {
|
|
const checkboxes = html.querySelectorAll('.sortilege-rune-checkbox:checked')
|
|
const modeSelect = html.querySelector('[name="runemode"]')
|
|
const mode = modeSelect?.value ?? "prononcer"
|
|
|
|
let maxSeuil = 0
|
|
let totalAme = 0
|
|
let totalActions = 0
|
|
let count = 0
|
|
|
|
// Enable/disable points inputs based on checkbox state
|
|
html.querySelectorAll('.sortilege-rune-checkbox').forEach(cb => {
|
|
const row = cb.closest('.sortilege-rune-row')
|
|
const pointsInput = row?.querySelector('.sortilege-rune-points')
|
|
if (pointsInput) pointsInput.disabled = !cb.checked
|
|
})
|
|
|
|
checkboxes.forEach(cb => {
|
|
const row = cb.closest('.sortilege-rune-row')
|
|
const seuil = Number(row?.dataset.seuil ?? 0)
|
|
const pts = Number(row?.querySelector('.sortilege-rune-points')?.value ?? 1)
|
|
if (seuil > maxSeuil) maxSeuil = seuil
|
|
totalAme += pts
|
|
totalActions += Math.ceil(pts / 3) * (mode === "inscrire" ? 2 : 1)
|
|
count++
|
|
})
|
|
|
|
const difficulte = count > 0 ? maxSeuil + (count - 1) : 0
|
|
html.querySelector('#sortilege-difficulte').textContent = count > 0 ? difficulte : '—'
|
|
html.querySelector('#sortilege-total-ame').textContent = totalAme
|
|
html.querySelector('#sortilege-actions').textContent = totalActions
|
|
}
|
|
|
|
html.querySelectorAll('.sortilege-rune-checkbox, .sortilege-rune-points, [name="runemode"]')
|
|
.forEach(el => el.addEventListener('change', recalculate))
|
|
}
|
|
|
|
/**
|
|
* Extract sortilège data from the form
|
|
* @param {Object} rollData
|
|
* @param {HTMLFormControlsCollection} formElements
|
|
* @param {MournbladeActor} actor
|
|
*/
|
|
static _updateSortilegeRollData(rollData, formElements, actor) {
|
|
rollData.runemode = formElements.runemode?.value ?? "prononcer"
|
|
rollData.modificateur = Number(formElements.modificateur?.value ?? 0)
|
|
rollData.runeautocible = formElements.runeautocible?.checked ?? false
|
|
|
|
// Collect selected runes with their soul points
|
|
const runes = actor.getRunes()
|
|
rollData.sortilegeRunes = []
|
|
for (const rune of runes) {
|
|
const checkbox = formElements[`rune-selected-${rune._id}`]
|
|
if (checkbox?.checked) {
|
|
const pts = Math.max(1, Number(formElements[`rune-points-${rune._id}`]?.value ?? 1))
|
|
rollData.sortilegeRunes.push({
|
|
id: rune._id,
|
|
name: rune.name,
|
|
img: rune.img,
|
|
seuil: rune.system.seuil,
|
|
formule: rune.system.formule,
|
|
pts,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (rollData.sortilegeRunes.length === 0) return
|
|
|
|
const maxSeuil = Math.max(...rollData.sortilegeRunes.map(r => r.seuil))
|
|
rollData.difficulte = maxSeuil + (rollData.sortilegeRunes.length - 1)
|
|
rollData.runeame = rollData.sortilegeRunes.reduce((s, r) => s + r.pts, 0)
|
|
}
|
|
|
|
/**
|
|
* @param {Object} rollData - L'objet rollData à mettre à jour
|
|
* @param {HTMLFormControlsCollection} formElements - Les éléments du formulaire
|
|
* @param {MournbladeActor} actor - L'acteur pour récupérer les attributs
|
|
* @private
|
|
*/
|
|
static _updateRollDataFromForm(rollData, formElements, actor) {
|
|
// Attributs
|
|
if (formElements.attrKey) {
|
|
rollData.attrKey = formElements.attrKey.value
|
|
if (rollData.attrKey !== "tochoose" && actor) {
|
|
rollData.attr = foundry.utils.duplicate(actor.system.attributs[rollData.attrKey])
|
|
rollData.actionImg = "systems/fvtt-mournblade/assets/icons/" + actor.system.attributs[rollData.attrKey].labelnorm + ".webp"
|
|
}
|
|
}
|
|
|
|
// Modificateurs de base
|
|
if (formElements.difficulte) {
|
|
rollData.difficulte = Number(formElements.difficulte.value)
|
|
}
|
|
if (formElements.modificateur) {
|
|
rollData.modificateur = Number(formElements.modificateur.value)
|
|
}
|
|
|
|
// Runes
|
|
if (formElements.runemode) {
|
|
rollData.runemode = String(formElements.runemode.value)
|
|
}
|
|
if (formElements.runeame) {
|
|
rollData.runeame = Number(formElements.runeame.value)
|
|
}
|
|
if (formElements.runeautocible !== undefined) {
|
|
rollData.runeautocible = formElements.runeautocible.checked
|
|
}
|
|
|
|
// Combat mêlée
|
|
if (formElements.typeAttaque) {
|
|
rollData.typeAttaque = String(formElements.typeAttaque.value)
|
|
}
|
|
if (formElements.isMonte !== undefined) {
|
|
rollData.isMonte = formElements.isMonte.checked
|
|
}
|
|
|
|
// Combat distance
|
|
if (formElements.visee !== undefined) {
|
|
rollData.visee = formElements.visee.checked
|
|
}
|
|
if (formElements.cibleconsciente !== undefined) {
|
|
rollData.cibleconsciente = formElements.cibleconsciente.checked
|
|
}
|
|
if (formElements.ciblecourt !== undefined) {
|
|
rollData.ciblecourt = formElements.ciblecourt.checked
|
|
}
|
|
if (formElements.typeCouvert) {
|
|
rollData.typeCouvert = String(formElements.typeCouvert.value)
|
|
}
|
|
|
|
// Désavantages
|
|
if (!rollData.desavantages) rollData.desavantages = {}
|
|
|
|
if (formElements.cibleausol !== undefined) {
|
|
rollData.desavantages.cibleausol = formElements.cibleausol.checked
|
|
}
|
|
if (formElements.cibledesarmee !== undefined) {
|
|
rollData.desavantages.cibledesarmee = formElements.cibledesarmee.checked
|
|
}
|
|
if (formElements.ciblerestreint !== undefined) {
|
|
rollData.desavantages.ciblerestreint = formElements.ciblerestreint.checked
|
|
}
|
|
if (formElements.cibleimmobilisée !== undefined) {
|
|
rollData.desavantages.cibleimmobilisée = formElements.cibleimmobilisée.checked
|
|
}
|
|
if (formElements.ciblesurplomb !== undefined) {
|
|
rollData.desavantages.ciblesurplomb = formElements.ciblesurplomb.checked
|
|
}
|
|
|
|
// Double D20
|
|
if (formElements.doubleD20 !== undefined) {
|
|
rollData.doubleD20 = formElements.doubleD20.checked
|
|
}
|
|
|
|
// Modifiers
|
|
if (rollData.modifiers) {
|
|
rollData.modifiers.forEach((modifier, idx) => {
|
|
const checkbox = formElements[`apply-modifier-${idx}`]
|
|
if (checkbox) {
|
|
modifier.system.apply = checkbox.checked
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|