Files
foundryvtt-reve-de-dragon/module/dialog-lancer-recette.js
T
fabres d0a434fb0b refactor(styles): migre toutes les directives CSS inline vers LESS
- Remplace les vieilles règles table (mortes) des dialog-gerer-herbes
  et dialog-gerer-ingredients par des règles flex scoped
- Dialog-gerer-herbes/potions/ingredients/deniers : style= flex/gap/padding
  → classes LESS par dialog avec nth-child
- actor-sheet.hbs : gap+flex-wrap → .oeuvre-buttons,
  height:200px → .tab.hautreve
- carac-derivee.hbs : style sur .sust-buttons → déplacé dans LESS
- regles-optionnelles.hbs : width:100% → .btn-voir-tables-encaissement
- dialog-lancer-recette (hbs+js) : status colors → classes .status-*
- roll-part-appelmoral.hbs : font-size conditionnel → classe .moral-low
- rdd-utility.js : span style → .resultat-created
- rdd-import-stats.js : textarea style → #statBlock
- base-actor-sheet.js : select style → .qty-amount
- export-scriptarium/*.hbs : flex-grow, margin-bottom → classes
- app-personnage-aleatoire.hbs : text-align → .random-char-right

Seuls 2 styles dynamiques persistent : background-image option
(partial-roll-coeur.hbs) et width fillPercent barre (inventaire-item.hbs).
2026-07-22 00:29:56 +02:00

229 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Grammar } from "./grammar.js"
import { ITEM_TYPES, renderTemplate } from "./constants.js"
import { Misc } from "./misc.js"
import { RdDAlchimie } from "./rdd-alchimie.js"
import { RdDCarac } from "./rdd-carac.js"
import { RdDRoll } from "./rdd-roll.js"
import { extraireEtapesAlchimie } from "./roll/extraire-etapes-alchimie.mjs"
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api
export class DialogLancerRecette extends HandlebarsApplicationMixin(ApplicationV2) {
static DEFAULT_OPTIONS = {
tag: "form",
classes: ["dialog-lancer-recette"],
form: { submitOnChange: false, closeOnSubmit: false },
position: { width: 650, height: "auto" },
window: { title: "Lancer une recette", icon: "fa-solid fa-flask" },
actions: {
lancerOperations: DialogLancerRecette.#onLancerOperations,
},
}
static PARTS = {
form: { template: "systems/foundryvtt-reve-de-dragon/templates/item/dialog-lancer-recette.hbs" },
}
static async create(item, actor) {
const dialog = new DialogLancerRecette({ item, actor })
dialog.render(true)
}
constructor(options) {
super(options)
this.item = options.item
this.actor = options.actor
this.etapes = []
this.enCours = false
this.termine = false
this.allReussi = false
this.consomme = false
}
/* -------------------------------------------- */
async _prepareContext() {
const ingredients = Misc.safeJsonParse(this.item.system.ingredients, [])
const inventoryItems = this.actor.items.filter(i =>
[ITEM_TYPES.herbe, ITEM_TYPES.plante, ITEM_TYPES.ingredient,
ITEM_TYPES.objet, ITEM_TYPES.nourritureboisson, ITEM_TYPES.faune,
ITEM_TYPES.potion].includes(i.type))
const ingredientRows = ingredients.map(ing => {
const matches = inventoryItems.filter(inv => Grammar.matchName(ing.nom, inv.name))
const foundQty = matches.reduce((sum, inv) => sum + (parseInt(inv.system.quantite) || 0), 0)
const requiredQty = parseInt(ing.quantite) || 1
return {
nom: ing.nom,
quantiteRequise: requiredQty,
quantiteTrouvee: foundQty,
unite: ing.unite,
consommable: ing.consommable,
suffisant: foundQty >= requiredQty,
}
})
if (!this.etapes.length) {
this.etapes = extraireEtapesAlchimie(this.item.system.manipulation || '')
for (const [i, e] of this.etapes.entries()) {
e.num = i + 1
e.status = 'pending'
e.caracValue = this.actor.system.carac[e.caracKey]?.value ?? 0
e.sansCristal = e.type === 'couleur' && !this.actor.items.some(it =>
it.type === 'objet' && Grammar.toLowerCaseNoAccent(it.name) === 'cristal alchimique' && it.system.quantite > 0)
e.diffConditions = e.sansCristal ? -4 : 0
}
}
const resultat = Misc.safeJsonParse(this.item.system.resultat, null)
return {
recette: this.item.name,
personnage: this.actor.name,
but: this.item.system.but,
ingredients: ingredientRows,
allIngredientsOk: ingredientRows.every(r => r.suffisant),
etapes: this.etapes,
enCours: this.enCours,
termine: this.termine,
allReussi: this.allReussi,
peutLancer: !this.enCours && !this.termine,
resultat,
}
}
/* -------------------------------------------- */
async _onRender(context, options) {
}
/* -------------------------------------------- */
static async #onLancerOperations(event, target) {
if (this.enCours) return
this.enCours = true
this.render(true)
for (let i = 0; i < this.etapes.length; i++) {
const etape = this.etapes[i]
etape.status = 'rolling'
this._updateEtapeRow(i)
const rolled = await this._effectuerEtape(etape)
etape.status = rolled.isSuccess ? 'success' : 'failure'
this._updateEtapeRow(i)
if (!rolled.isSuccess) break
}
this.enCours = false
this.termine = true
this.allReussi = this.etapes.every(e => e.status === 'success')
await this._consommerIngredients()
this.consomme = true
this.render(true)
if (this.allReussi) {
ui.notifications.info('Toutes les opérations alchimiques ont réussi!')
const resultat = Misc.safeJsonParse(this.item.system.resultat, null)
if (resultat && resultat.type) {
const msg = await renderTemplate('systems/foundryvtt-reve-de-dragon/templates/chat-resultat-alchimie-reussite.hbs', {
recette: this.item.name,
resultat,
resultatJson: JSON.stringify(resultat),
actorId: this.actor.id,
recipeImg: this.item.img,
alias: this.actor.getAlias ? this.actor.getAlias() : this.actor.name,
})
await ChatMessage.create({
content: msg,
speaker: ChatMessage.getSpeaker({ actor: this.actor }),
})
}
} else {
ui.notifications.warn('La recette a échoué. Les ingrédients ont été consommés.')
}
}
/* -------------------------------------------- */
async _effectuerEtape(etape) {
const tacheAlchimie = etape.type
const texteTache = etape.termes
const sansCristal = etape.sansCristal
const caracTache = RdDAlchimie.getCaracTache(tacheAlchimie)
const alchimieData = this.actor.getCompetence('alchimie')
const rollData = {
recette: this.item,
carac: { [caracTache]: this.actor.system.carac[caracTache] },
selectedCarac: this.actor.system.carac[caracTache],
competence: alchimieData,
diffLibre: RdDAlchimie.getDifficulte(texteTache),
diffConditions: sansCristal ? -4 : 0,
alchimie: {
tache: Misc.upperFirst(tacheAlchimie),
texte: texteTache,
sansCristal,
},
}
if (rollData.competence) {
rollData.competence.system.defaut_carac = caracTache
}
return new Promise(resolve => {
const callbacks = []
if (this.actor.createCallbackExperience) callbacks.push(this.actor.createCallbackExperience())
if (this.actor.createCallbackAppelAuMoral) callbacks.push(this.actor.createCallbackAppelAuMoral())
callbacks.push({
action: async r => {
resolve(r.rolled)
if (this.actor._alchimieResult) await this.actor._alchimieResult(r)
},
})
RdDRoll.create(this.actor, rollData,
{ html: 'systems/foundryvtt-reve-de-dragon/templates/dialog-roll-alchimie.hbs' },
{ name: 'tache-alchimique', label: 'Tâche Alchimique', callbacks }
).then(dialog => {
dialog.render(true)
dialog.bringToFront()
})
})
}
/* -------------------------------------------- */
_updateEtapeRow(index) {
const etape = this.etapes[index]
if (!etape) return
const row = this.element?.querySelector(`.etape-row[data-index="${index}"]`)
if (!row) return
row.className = `etape-row etape-${etape.status}`
const cell = row.querySelector('.etape-status')
if (!cell) return
const icons = {
pending: '<i class="fas fa-circle status-pending"></i>',
rolling: '<i class="fas fa-spinner fa-pulse status-rolling"></i>',
success: '<i class="fas fa-check-circle status-success"></i>',
failure: '<i class="fas fa-times-circle status-failure"></i>',
}
cell.innerHTML = icons[etape.status] || ''
}
/* -------------------------------------------- */
async _consommerIngredients() {
const ingredients = Misc.safeJsonParse(this.item.system.ingredients, [])
for (const ing of ingredients) {
if (!ing.consommable) continue
const matches = this.actor.items.filter(inv => Grammar.matchName(ing.nom, inv.name))
let remaining = parseInt(ing.quantite) || 1
for (const item of matches) {
if (remaining <= 0) break
const qty = parseInt(item.system.quantite) || 0
if (qty <= 0) continue
const toTake = Math.min(qty, remaining)
await item.diminuerQuantite(toTake, { supprimerSiZero: true })
remaining -= toTake
}
}
}
}