import { ACTOR_TYPES, ITEM_TYPES } from "../../constants.js" import { Grammar } from "../../grammar.js" import { RdDItemArme } from "../../item/arme.js" import { CATEGORIES_COMPETENCE_COMBAT } from "../../item/base-items.js" import { Misc } from "../../misc.js" import { RdDTimestamp } from "../../time/rdd-timestamp.js" import { PDFDocument } from "./pdf-lib/pdf-lib.esm.js" const copyProperty = (actor, path) => foundry.utils.getProperty(actor, path) // const findItem = (actor, itemType, itemName) => actor.itemTypes[itemType].find(it => Grammar.equalsInsensitive(formCompName(it.name), formCompName(itemName))) // const findItemPos = (actor, itemType, pos) => actor.itemTypes[itemType].length <= pos ? actor.itemTypes[itemType][pos] : length // const findProperty = (it, path) => it ? foundry.utils.getProperty(it, path) : undefined // const findItemProperty = (actor, itemType, itemName, path) => findProperty(findItem(actor, itemType, itemName), path) // const findItemPosProperty = (actor, itemType, pos, path) => findProperty(findItemPos(actor, itemType, pos), path) // const findArmeProperty = (actor, pos, path) => findProperty(findItemPos(actor, ITEM_TYPES.arme, pos), path) // const findSortProperty = (actor, pos, path) => findProperty(findItemPos(actor, ITEM_TYPES.sort, pos), path) // const itemFormPath = (type, pos, property) => `${type}s.${pos}.${property}` const ACTOR_TO_FORM_MAPPING = [ { path: 'name' }, { path: 'system.carac.taille.value' }, { path: 'system.carac.apparence.value' }, { path: 'system.carac.apparence.xp' }, { path: 'system.carac.constitution.value' }, { path: 'system.carac.constitution.xp' }, { path: 'system.carac.force.value' }, { path: 'system.carac.force.value' }, { path: 'system.carac.force.xp' }, { path: 'system.carac.agilite.value' }, { path: 'system.carac.agilite.xp' }, { path: 'system.carac.dexterite.value' }, { path: 'system.carac.dexterite.xp' }, { path: 'system.carac.vue.value' }, { path: 'system.carac.vue.xp' }, { path: 'system.carac.ouie.value' }, { path: 'system.carac.ouie.xp' }, { path: 'system.carac.odoratgout.value' }, { path: 'system.carac.odoratgout.xp' }, { path: 'system.carac.volonte.value' }, { path: 'system.carac.volonte.xp' }, { path: 'system.carac.empathie.value' }, { path: 'system.carac.empathie.xp' }, { path: 'system.carac.intellect.value' }, { path: 'system.carac.intellect.xp' }, { path: 'system.carac.reve.value' }, { path: 'system.carac.reve.xp' }, { path: 'system.carac.chance.value' }, { path: 'system.carac.chance.xp' }, { path: 'system.age' }, { path: 'system.sexe' }, { path: 'system.taille' }, { path: 'system.poids' }, { path: 'system.cheveux' }, { path: 'system.yeux' }, { path: 'system.beaute' }, { path: 'system.main' }, { path: 'system.heure' }, { path: 'computed.hn.heure', getter: actor => (RdDTimestamp.definition(actor.system.heure)?.heure ?? 0) + 1 }, { path: 'computed.hn.label', getter: actor => RdDTimestamp.definition(actor.system.heure)?.avecArticle }, { path: 'system.carac.melee.value' }, { path: 'system.carac.tir.value' }, { path: 'system.carac.lancer.value' }, { path: 'system.carac.derobee.value' }, { path: 'system.sante.vie.value' }, { path: 'system.sante.endurance.value' }, { path: 'system.attributs.sust.value' }, { path: 'system.attributs.sconst.value' }, { path: 'system.attributs.encombrement.value' }, { path: 'system.attributs.plusdom.value', getter: actor => Misc.toSignedString(actor.system.attributs.plusdom.value) }, // , getter: actor => actor.get ] export default class ExportPdf { static init() { Hooks.on("getActorContextOptions", (actorDirectory, menus) => { ExportPdf.onActorDirectoryMenu(actorDirectory, menus) }) } static onActorDirectoryMenu(actorDirectory, menus) { menus.push({ name: 'Export PDF', icon: '', condition: target => actorDirectory.id == 'actors' && ExportPdf.$isActorPersonnage(this.$getActor(target)), callback: async target => await ExportPdf.exportActor(target) }) } static $getActor(target) { const entryId = $(target).closest(".directory-item")?.data("entryId") return game.actors.get(entryId) } static $isActorPersonnage(actor) { return actor?.type == ACTOR_TYPES.personnage } static async exportActor(target) { const actor = ExportPdf.$getActor(target) if (!ExportPdf.$isActorPersonnage(actor)) { ui.notifications.error("Pas de personnage sélectionné") return } const templatePdf = '/systems/foundryvtt-reve-de-dragon/assets/feuille-personnage.pdf'; const pdfBytes = await fetch(templatePdf).then(res => res.arrayBuffer()) const pdfDoc = await PDFDocument.load(pdfBytes) const exporter = new ExportPdf(actor, pdfDoc) exporter.generateFeuillePersonnage() } constructor(actor, pdfDoc) { this.pdfDoc = pdfDoc this.form = this.pdfDoc.getForm() this.actor = actor this.allComps = this.actor.itemTypes[ITEM_TYPES.competence] this.comps = this.allComps .filter(it => !it.isNiveauBase()) .sort(Misc.ascending(it => it.name)) this.compsNonArmes = this.comps.filter(it => !ExportPdf.isCompCombat(it)) this.compsArmes = this.comps.filter(it => ExportPdf.isCompCombat(it)) this.addedComps = new Set([]) } static isCompCombat(comp) { return CATEGORIES_COMPETENCE_COMBAT.includes(comp.system.categorie) && !Grammar.includesLowerCaseNoAccent(comp.name, "corps à corps") && !Grammar.includesLowerCaseNoAccent(comp.name, "esquive") } async generateFeuillePersonnage() { this.$exportActorFields() this.$exportCompetences() this.$exportArchetype() this.$exportArmes() this.$exportSorts() const pdfBytes = await this.pdfDoc.save(); const filename = `rdd-${this.actor.name.slugify()}.pdf`; foundry.utils.saveDataToFile(pdfBytes, "application/pdf", filename); } $exportActorFields() { ACTOR_TO_FORM_MAPPING.forEach(async (mapping) => { const path = mapping.path const value = mapping.getter ? mapping.getter(this.actor) : copyProperty(this.actor, path) this.$setFormValue(path, value) }) } $exportCompetences() { this.compsNonArmes .filter(it => !this.addedComps.has(it.id)) .forEach(comp => { const formCompName = Grammar.toLowerCaseNoAccent(comp.name.replaceAll(/(\s|-|\')/g, '_')) this.$setFormCompetence(formCompName, comp) }) const musique = this.compsNonArmes.filter(it => Grammar.includesLowerCaseNoAccent(it.name, 'musique')) .sort(Misc.descending(it => it.system.niveau)) .filter(it => !this.addedComps.has(it.id)) .find(it => true) if (musique) { this.$setFormCompetence('musique', musique) } } $exportArchetype() { this.allComps.sort(Misc.ascending(it => it.system.niveau_archetype)) .forEach(comp => { let formCompName = Grammar.toLowerCaseNoAccent(comp.name.replaceAll(/(\s|-|\')/g, '_')) if (formCompName.includes('musique')) { formCompName = 'musique' } this.$setFormValue(`competences.${formCompName}.niveau_archetype`, comp.system.niveau_archetype) }) } $setFormCompetenceArchetype(formCompName, comp, baseFormName = 'competences') { } $exportArmes() { const uniques = new Set([]) const armes = this.actor.itemTypes[ITEM_TYPES.arme].map(arme => arme.getTypeAttaques() .map(main => { const compName = arme.getCompetenceAction(main) const dommages = RdDItemArme.valeurMain(arme.system.dommages, main) const forceRequise = RdDItemArme.valeurMain(arme.system.force ?? 0, main) const comp = this.compsArmes.find(it => Grammar.equalsInsensitive(it.name, compName)) ?? this.actor.findItemLike(compName, ITEM_TYPES.competence) const unique = [comp.id, arme.name, dommages, forceRequise].join('|'); if (uniques.has(unique)) { return undefined } uniques.add(unique) return { arme: arme, comp: comp, main: main } })) .reduce((a, b) => a.concat(b)) .filter(it => it != undefined && !it.comp.isNiveauBase()) .sort(Misc.descending(it => it.comp.niveau)) for (let pos = 0; pos < armes.length; pos++) { const it = armes[pos] this.$setFormArmeCompetence(pos, it.comp, it.arme, it.main, it.main) } // TODO: list comps without weapons // TODO: list other comps not in the standard list -- use an instance of ExportPdf to hold state/built list const otherComps = this.comps.filter(it => !this.addedComps.has(it.id)) for (let pos = 0; pos < otherComps.length; pos++) { const comp = otherComps[pos] this.$setFormCompetence(pos, comp, 'competences') this.$setFormValue(`competences.${pos}.name`, comp.name) } } $setFormCompetence(formCompName, comp, baseFormName = 'competences') { if (this.form.getFieldMaybe(`${baseFormName}.${formCompName}.niveau`)) { this.addedComps.add(comp.id) } if (comp.system.niveau != comp.system.base) { this.$setFormValue(`${baseFormName}.${formCompName}.niveau`, comp.system.niveau) } if (comp.system.xp > 0) { this.$setFormValue(`${baseFormName}.${formCompName}.xp`, comp.system.xp) } if (comp.system.xp_sort > 0) { this.$setFormValue(`${baseFormName}.${formCompName}.xp_sort`, comp.system.xp_sort) } if (CATEGORIES_COMPETENCE_COMBAT.includes(comp.system.categorie)) { this.$setFormValue(`${baseFormName}.${formCompName}.init`, comp.getBaseInit()) } } $setFormArmeCompetence(pos, comp, arme, main) { this.$setFormCompetence(pos, comp, 'armes') this.$setFormValue(`armes.${pos}.name`, arme.name) this.$setFormValue(`armes.${pos}.main`, main) this.$setFormValue(`armes.${pos}.plusdom`, RdDItemArme.valeurMain(arme.system.dommages, main)) } $exportSorts() { const sorts = this.actor.itemTypes[ITEM_TYPES.sort].sort(Misc.ascending(s => ExportPdf.$orderDraconic(s) + s.name)) for (let pos = 0; pos < sorts.length; pos++) { const sort = sorts[pos] this.$setFormSort(pos, sort) } } $setFormSort(pos, sort) { this.$setFormValue(`sorts.${pos}.name`, sort.name) this.$setFormValue(`sorts.${pos}.voie`, sort.system.draconic) this.$setFormValue(`sorts.${pos}.tmr`, sort.system.caseTMRSpeciale ?? sort.system.caseTMR) this.$setFormValue(`sorts.${pos}.diff`, sort.system.difficulte) this.$setFormValue(`sorts.${pos}.reve`, sort.system.ptreve) this.$setFormValue(`sorts.${pos}.bonuscase`, sort.system.bonuscase) } static $orderDraconic(s) { switch (s.system.draconic.substring(0, 1)) { case 'O': return 1 case 'H': return 2 case 'N': return 3 case 'T': return 4 } return 5 } $setFormValue(path, value) { const hasField = this.form.getFieldMaybe(path) if (hasField && value != undefined) { const field = this.form.getTextField(path) field.setText(value.toString()) } } }