diff --git a/module/actor/export-scriptarium/export-scriptarium.js b/module/actor/export-scriptarium/export-scriptarium.js new file mode 100644 index 00000000..5cf6b165 --- /dev/null +++ b/module/actor/export-scriptarium/export-scriptarium.js @@ -0,0 +1,73 @@ +import { LOG_HEAD } from "../../constants.js" +import { ACTOR_TYPES } from "../../item.js" +import { Misc } from "../../misc.js" +import { EXPORT_CSV_SCRIPTARIUM, OptionsAvancees } from "../../settings/options-avancees.js" +import { Mapping } from "./mapping.js" + +const IMG_SCRIPTARIUM = '' + +export class ExportScriptarium { + + static init() { + ExportScriptarium.INSTANCE = new ExportScriptarium() + } + + constructor() { + this.mapping = Mapping.getMapping() + Hooks.on("getActorDirectoryFolderContext", (actorDirectory, menus) => { ExportScriptarium.INSTANCE.onActorDirectoryMenu(actorDirectory, menus) }) + Hooks.on("getActorDirectoryEntryContext", (actorDirectory, menus) => { ExportScriptarium.INSTANCE.onActorDirectoryMenu(actorDirectory, menus) }) + } + + onActorDirectoryMenu(actorDirectory, menus) { + menus.push({ + name: 'Export Personnages', + icon: IMG_SCRIPTARIUM, + condition: (target) => game.user.isGM && + OptionsAvancees.isUsing(EXPORT_CSV_SCRIPTARIUM) && + this.$getActors(actorDirectory, target).length > 0, + callback: target => this.exportActors(this.$getActors(actorDirectory, target)) + }) + } + + $getActors(actorDirectory, target) { + const li = target.closest(".directory-item") + const folderId = li.data("folderId") + const actorId = li.data("documentId") + const actors = actorId + ? [game.actors.get(actorId)] + : folderId + ? actorDirectory.folders.find(it => it.id == folderId).contents + : [] + return actors.filter(it => it.type == ACTOR_TYPES.personnage) + } + + exportActors(actors) { + console.log(LOG_HEAD + 'exportActors', actors) + const header = Misc.join(this.getHeaderLine(), ';') + console.log(header) + actors.forEach(actor => { + const actorLine = Misc.join(this.getActorLine(actor), ';') + console.log(actorLine) + }) + } + + getHeaderLine() { + return this.mapping.map(it => it.column) + } + + getActorLine(actor) { + const context = Mapping.prepareContext(actor) + return this.mapping.map(it => it.getter(actor, context)) + .map(it => this.$escapeQuotes(it)) + .map(it => it.replaceAll("\n", " ").replaceAll("\r", "")) + } + + $escapeQuotes(it) { + it = '' + it + if (it.includes('"') || it.includes(';')) { + return `"${it.replaceAll('"', '\\"')}"` + } + return it + } + +} diff --git a/module/actor/export-scriptarium/mapping.js b/module/actor/export-scriptarium/mapping.js new file mode 100644 index 00000000..567645d8 --- /dev/null +++ b/module/actor/export-scriptarium/mapping.js @@ -0,0 +1,303 @@ +import { Grammar } from "../../grammar.js" +import { RdDItemArme } from "../../item-arme.js" +import { RdDItemCompetence } from "../../item-competence.js" +import { RdDItemSort } from "../../item-sort.js" +import { ITEM_TYPES } from "../../item.js" +import { Misc } from "../../misc.js" +import { RdDTimestamp } from "../../time/rdd-timestamp.js" +import { TMRConstants } from "../../tmr-constants.js" +import { TMRUtility } from "../../tmr-utility.js" + +const CATEGORIES_COMPETENCES = [ + "generale", + "particuliere", + "specialisee", + "connaissance", +] +const CATEGORIES_DRACONIC = [ + "draconic", +] + +const CATEGORIES_COMBAT = [ + "melee", + "tir", + "lancer" +] + +const NIVEAU_BASE = { + "generale": -4, + "particuliere": -8, + "specialisee": -11, + "connaissance": -11, + "draconic": -11, + "melee": -6, + "tir": -8, + "lancer": -8, +} + +class ColumnMappingFactory { + static createMappingArme(part, i) { + return { column: `arme-${part}-${i}`, getter: (actor, context) => Mapping.getArme(actor, context, part, i) } + } + + static createMappingSort(part, i) { + return { column: `sort-${part}-${i}`, getter: (actor, context) => Mapping.getSort(actor, context, part, i) } + } +} + +const NB_ARMES = 10 +const NB_SORTS = 20 +const TABLEAU_ARMES = [...Array(NB_ARMES).keys()] +const TABLEAU_SORTS = [...Array(NB_SORTS).keys()] + +const MAPPING_BASE = [ + { column: "ID", getter: (actor, context) => actor.id }, + { column: "name", getter: (actor, context) => actor.name }, + // { column: "biographie", getter: (actor, context) => actor.system.biographie }, + { column: "taille", getter: (actor, context) => actor.system.carac.taille.value }, + { column: "apparence", getter: (actor, context) => actor.system.carac.apparence.value }, + { column: "constitution", getter: (actor, context) => actor.system.carac.constitution.value }, + { column: "force", getter: (actor, context) => actor.system.carac.force.value }, + { column: "agilite", getter: (actor, context) => actor.system.carac.agilite.value }, + { column: "dexterite", getter: (actor, context) => actor.system.carac.dexterite.value }, + { column: "vue", getter: (actor, context) => actor.system.carac.vue.value }, + { column: "ouie", getter: (actor, context) => actor.system.carac.ouie.value }, + { column: "odoratgout", getter: (actor, context) => actor.system.carac.odoratgout.value }, + { column: "volonte", getter: (actor, context) => actor.system.carac.volonte.value }, + { column: "intellect", getter: (actor, context) => actor.system.carac.intellect.value }, + { column: "empathie", getter: (actor, context) => actor.system.carac.empathie.value }, + { column: "reve", getter: (actor, context) => actor.system.carac.reve.value }, + { column: "chance", getter: (actor, context) => actor.system.carac.chance.value }, + { column: "melee", getter: (actor, context) => actor.system.carac.melee.value }, + { column: "tir", getter: (actor, context) => actor.system.carac.tir.value }, + { column: "lancer", getter: (actor, context) => actor.system.carac.lancer.value }, + { column: "derobee", getter: (actor, context) => actor.system.carac.derobee.value }, + { column: "vie", getter: (actor, context) => actor.system.sante.vie.max }, + { column: "plusdom", getter: (actor, context) => actor.system.attributs.plusdom.value }, + { column: "protectionnaturelle", getter: (actor, context) => actor.system.attributs.protection.value }, + { column: "endurance", getter: (actor, context) => actor.system.sante.endurance.max }, + { column: "description", getter: (actor, context) => Mapping.getDescription(actor) }, + { column: "armure", getter: (actor, context) => Mapping.getArmure(actor, context) }, + { column: "protection", getter: (actor, context) => Mapping.getProtectionArmure(actor, context) }, + { column: "malus-armure", getter: (actor, context) => Mapping.getMalusArmure(actor, context) }, + { column: "esquive", getter: (actor, context) => Mapping.getEsquive(actor, context) }, + { column: "esquive-niv", getter: (actor, context) => Mapping.getEsquiveNiveau(context) }, + { column: "competences", getter: (actor, context) => Mapping.getCompetences(actor, CATEGORIES_COMPETENCES) }, + { column: "draconic", getter: (actor, context) => Mapping.getCompetences(actor, CATEGORIES_DRACONIC) }, +] + +const MAPPING_ARMES = TABLEAU_ARMES.map(i => ColumnMappingFactory.createMappingArme('name', i)) + .concat(TABLEAU_ARMES.map(i => ColumnMappingFactory.createMappingArme('niveau', i))) + .concat(TABLEAU_ARMES.map(i => ColumnMappingFactory.createMappingArme('init', i))) + .concat(TABLEAU_ARMES.map(i => ColumnMappingFactory.createMappingArme('dom', i))) +const MAPPING_SORTS = TABLEAU_SORTS.map(i => ColumnMappingFactory.createMappingSort('voie', i)) + .concat(TABLEAU_SORTS.map(i => ColumnMappingFactory.createMappingSort('description', i))) + .concat(TABLEAU_SORTS.map(i => ColumnMappingFactory.createMappingSort('bonus', i))) +const MAPPING = MAPPING_BASE + .concat(MAPPING_ARMES) + .concat(MAPPING_SORTS) + +export class Mapping { + + static getMapping() { + return MAPPING + } + + static prepareContext(actor) { + return { + armes: Mapping.prepareArmes(actor), + armure: Mapping.prepareArmure(actor), + esquive: Mapping.prepareEsquive(actor), + sorts: Mapping.prepareSorts(actor) + } + } + + static prepareArmes(actor) { + return actor.items.filter(it => it.type == ITEM_TYPES.arme) + .map(arme => { + const compToUse = RdDItemArme.getCompetenceArme(arme, 'competence'); + const comp = actor.getCompetence(compToUse); + const bonusDom = Mapping.calculBonusDom(comp, actor) + return { + name: arme.name, + niveau: comp.system.niveau, + init: Mapping.calculBaseInit(actor, comp.system.categorie) + comp.system.niveau, + dom: Number(arme.system.dommages) + bonusDom + }; + }); + } + + static calculBonusDom(comp, actor) { + // TODO: reuse dmg calc? + const appliesBonusDom = ['melee', 'lancer'].includes(comp.system.categorie) + return appliesBonusDom ? Number(actor.system.attributs.plusdom.value) : 0 + } + + static calculBaseInit(actor, categorie) { + // TODO: reuse init calc? + const mapping = MAPPING_BASE.find(it => it.column == categorie) + if (mapping) { + switch (categorie) { + case 'melee': + case 'tir': + case 'lancer': + const caracteristique = Number(actor.system.carac[categorie].value) + return Math.floor(caracteristique / 2) + } + } + return 0 + } + + static prepareArmure(actor) { + const armures = actor.itemTypes[ITEM_TYPES.armure].filter(it => it.system.equipe) + if (armures.length > 1) { + console.warn(`${actor.name} a équipé ${armures.length} armures, seule la première sera considérée`) + } + if (armures.length > 0) { + const armure = armures[0] + return { + name: armure.name, + protection: armure.system.protection, + malus: armure.system.malus ?? 0 + } + } + return { + name: '', + protection: actor.system.attributs.protection.value, + malus: 0 + } + } + + static prepareEsquive(actor) { + const esquives = actor.getCompetences("Esquive") + if (esquives.length > 0) { + const esquive = esquives[0] + return { + name: esquive.name, + niveau: esquive.system.niveau + } + } + return undefined + } + + static prepareSorts(actor) { + return actor.itemTypes[ITEM_TYPES.sort].map(it => { + return { + voie: it.system.voie, + description: Mapping.descriptionSort(it), + bonus: Mapping.bonusCase(it) + } + }) + } + + static descriptionSort(sort) { + const ptSeuil = Array(sort.system.coutseuil).map(it => '*') + const caseTMR = sort.system.caseTMRspeciale.length > 0 ? sort.system.caseTMRspeciale : sort.system.caseTMR + return `${sort.name}${ptSeuil} (${caseTMR}) R${sort.system.difficulte} r${sort.system.ptreve}` + } + + static bonusCase(sort) { + const list = RdDItemSort.buildBonusCaseList(sort.system.bonuscase, false).sort(Misc.descending(it => it.bonus)) + if (list.length > 0) { + const bonus = list[0] + return `+${bonus.bonus}% en ${bonus.case}` + } + } + + static getDescription(actor) { + const sexe = actor.system.sexe + const sexeFeminin = sexe.length > 0 && sexe.charAt(0).toLowerCase() == 'f' ? 'Née' : 'Né' + const race = ['', 'humain'].includes(Grammar.toLowerCaseNoAccent(actor.system.race)) ? '' : (actor.system.race + ' ') + const heure = actor.system.heure + const hn = `${sexeFeminin} à l'heure ${RdDTimestamp.definition(heure).avecArticle}` + const age = actor.system.age ? `${actor.system.age} ans` : undefined + const taille = actor.system.taille + const poids = actor.system.poids + const cheveux = actor.system.cheveux ? `cheveux ${actor.system.cheveux}` : undefined + const yeux = actor.system.yeux ? `yeux ${actor.system.yeux}` : undefined + const beaute = actor.system.beaute ? `Beauté ${actor.system.beaute}` : undefined + const list = [race, hn, age, taille, poids, cheveux, yeux, beaute] + return Misc.join(list.filter(it => it), ', ') + } + + static getArmure(actor, context) { + return context.armure?.name ?? '' + } + + static getProtectionArmure(actor, context) { + return Number(context?.armure?.protection ?? 0) + Number(actor.system.attributs.protection.value) + } + + static getMalusArmure(actor, context) { + return context?.armure?.malus ?? 0 + } + + static getEsquive(actor, context) { + return context.esquive?.name ?? '' + } + + static getEsquiveNiveau(context) { + if (context.esquive) { + const niveau = context.esquive.niveau + context.armure.malus + return niveau > 0 ? ('+' + niveau) : ('' + niveau) + } + return '' + } + + static getCompetences(actor, categories) { + const competences = Mapping.getCompetencesCategorie(actor, categories) + if (competences.length == 0) { + return '' + } + const byCartegories = Mapping.competencesByCategoriesByNiveau(competences, categories) + const txtByCategories = Object.values(byCartegories) + .map(it => it.competencesParNiveau) + .map(byNiveau => { + const niveaux = Object.keys(byNiveau).map(it => Number(it)).sort(Misc.ascending()) + if (niveaux.length == 0) { + return '' + } + const txtCategorieByNiveau = niveaux.map(niveau => { + const names = Misc.join(byNiveau[niveau].map(it => it.name).sort(Misc.ascending()), ', ') + return names + ': ' + Misc.toSignedString(niveau) + } + ) + const txtCategorie = Misc.join(txtCategorieByNiveau, ' / ') + return txtCategorie + }).filter(it => it != '') + + return Misc.join(txtByCategories, ' / ') + } + + static competencesByCategoriesByNiveau(competences, categories) { + return categories.map(c => { + return { + categorie: c, + competencesParNiveau: Misc.classify( + competences.filter(comp => comp.system.categorie == c), + comp => comp.system.niveau) + } + }) + } + + static getArme(actor, context, part, numero) { + if (numero < context.armes.length) { + return context.armes[numero][part] ?? '' + } + return '' + } + + static getCompetencesCategorie(actor, categories) { + return actor.itemTypes[ITEM_TYPES.competence] + .filter(it => categories.includes(it.system.categorie)) + .filter(it => !RdDItemCompetence.isNiveauBase(it)) + } + + static getSort(actor, context, part, numero) { + if (numero < context.sorts.length) { + return context.sorts[numero][part] + } + return '' + } +} + diff --git a/module/rdd-main.js b/module/rdd-main.js index e16e7948..ffcf00e8 100644 --- a/module/rdd-main.js +++ b/module/rdd-main.js @@ -65,6 +65,8 @@ import { AutoAdjustDarkness as AutoAdjustDarkness } from "./time/auto-adjust-dar import { RdDCreature } from "./actor/creature.js" import { RdDTMRDialog } from "./rdd-tmr-dialog.js" import { RdDActorExportSheet } from "./actor/actor-export-sheet.js" +import { OptionsAvancees } from "./settings/options-avancees.js" +import { ExportScriptarium } from "./actor/export-scriptarium/export-scriptarium.js" /** * RdD system @@ -197,6 +199,7 @@ export class SystemReveDeDragon { SystemCompendiums.init() DialogChronologie.init() ReglesOptionnelles.init() + OptionsAvancees.init() RdDUtility.init() RdDDice.init() RdDCommands.init() @@ -211,6 +214,7 @@ export class SystemReveDeDragon { RdDPossession.init() TMRRencontres.init() Environnement.init() + ExportScriptarium.init() } initSystemSettings() { diff --git a/module/settings/options-avancees.js b/module/settings/options-avancees.js new file mode 100644 index 00000000..1e4cad4e --- /dev/null +++ b/module/settings/options-avancees.js @@ -0,0 +1,91 @@ +import { SYSTEM_RDD } from "../constants.js" +import { Misc } from "../misc.js" + +export const EXPORT_CSV_SCRIPTARIUM = 'export-csv-scriptarium' + +const OPTIONS_AVANCEES = [ + { group: 'Menus', name: EXPORT_CSV_SCRIPTARIUM, descr: "Proposer le menu d'export csv Scriptarium (raffraichissement requis)" }, +] + +export class OptionsAvancees extends FormApplication { + static init() { + for (const regle of OPTIONS_AVANCEES) { + const name = regle.name + const id = OptionsAvancees._getId(name) + game.settings.register(SYSTEM_RDD, id, { name: id, scope: regle.scope ?? "world", config: false, default: regle.default == undefined ? true : regle.default, type: Boolean }) + } + + game.settings.registerMenu(SYSTEM_RDD, "rdd-options-avancees", { + name: "Configurer les options avancées", + label: "Options avancées", + hint: "Ouvre la fenêtre de configuration des options avancées", + icon: "fas fa-bars", + type: OptionsAvancees + }) + } + + constructor(...args) { + super(...args) + } + + static _getId(name) { + return `rdd-advanced-${name}` + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + id: "options-avancees", + template: "systems/foundryvtt-reve-de-dragon/templates/settings/options-avancees.hbs", + height: 650, + width: 550, + minimizable: false, + closeOnSubmit: true, + title: "Options avancées" + }, { inplace: false }) + } + + getData() { + let formData = super.getData() + const regles = OPTIONS_AVANCEES.filter(it => game.user.isGM || it.scope == "client") + .map(it => { + it = foundry.utils.duplicate(it) + it.id = OptionsAvancees._getId(it.name) + it.active = OptionsAvancees.isSet(it.name) + return it + }) + formData.regles = regles + formData.groups = Misc.classify(regles, it => it.group) + return formData + } + + static getSettingKey(name){ + return `${SYSTEM_RDD}.${this._getId(name)}` + } + + static isUsing(name) { + return OptionsAvancees.isSet(name) + } + + static isSet(name) { + return game.settings.get(SYSTEM_RDD, OptionsAvancees._getId(name)) + } + + static set(name, value) { + return game.settings.set(SYSTEM_RDD, OptionsAvancees._getId(name), value ? true : false) + } + + activateListeners(html) { + html.find(".select-option").click((event) => { + if (event.currentTarget.attributes.name) { + let id = event.currentTarget.attributes.name.value + let isChecked = event.currentTarget.checked + game.settings.set(SYSTEM_RDD, id, isChecked) + } + }) + } + + async _updateObject(event, formData) { + this.close() + } +} + diff --git a/module/time/rdd-timestamp.js b/module/time/rdd-timestamp.js index 2f12d5c3..c10d55f6 100644 --- a/module/time/rdd-timestamp.js +++ b/module/time/rdd-timestamp.js @@ -15,18 +15,18 @@ export const RDD_MINUTES_PAR_JOUR = 1440; //RDD_HEURES_PAR_JOUR * RDD_MINUTES_PA const ROUNDS_PAR_MINUTE = 10; const DEFINITION_HEURES = [ - { key: "vaisseau", label: "Vaisseau", lettreFont: 'v', saison: "Printemps", darkness: 0.9 }, - { key: "sirene", label: "Sirène", lettreFont: 'i', saison: "Printemps", darkness: 0.1 }, - { key: "faucon", label: "Faucon", lettreFont: 'f', saison: "Printemps", darkness: 0 }, - { key: "couronne", label: "Couronne", lettreFont: '', saison: "Eté", darkness: 0 }, - { key: "dragon", label: "Dragon", lettreFont: 'd', saison: "Eté", darkness: 0 }, - { key: "epees", label: "Epées", lettreFont: 'e', saison: "Eté", darkness: 0 }, - { key: "lyre", label: "Lyre", lettreFont: 'l', saison: "Automne", darkness: 0.1 }, - { key: "serpent", label: "Serpent", lettreFont: 's', saison: "Automne", darkness: 0.9 }, - { key: "poissonacrobate", label: "Poisson Acrobate", lettreFont: 'p', saison: "Automne", darkness: 1 }, - { key: "araignee", label: "Araignée", lettreFont: 'a', saison: "Hiver", darkness: 1 }, - { key: "roseau", label: "Roseau", lettreFont: 'r', saison: "Hiver", darkness: 1 }, - { key: "chateaudormant", label: "Château Dormant", lettreFont: 'c', saison: "Hiver", darkness: 1 }, + { key: "vaisseau", article: "du ", label: "Vaisseau", lettreFont: 'v', saison: "Printemps", darkness: 0.9 }, + { key: "sirene", article: "de la ", label: "Sirène", lettreFont: 'i', saison: "Printemps", darkness: 0.1 }, + { key: "faucon", article: "du ", label: "Faucon", lettreFont: 'f', saison: "Printemps", darkness: 0 }, + { key: "couronne", article: "de la ", label: "Couronne", lettreFont: '', saison: "Eté", darkness: 0 }, + { key: "dragon", article: "du ", label: "Dragon", lettreFont: 'd', saison: "Eté", darkness: 0 }, + { key: "epees", article: "des ", label: "Epées", lettreFont: 'e', saison: "Eté", darkness: 0 }, + { key: "lyre", article: "de la ", label: "Lyre", lettreFont: 'l', saison: "Automne", darkness: 0.1 }, + { key: "serpent", article: "du ", label: "Serpent", lettreFont: 's', saison: "Automne", darkness: 0.9 }, + { key: "poissonacrobate", article: "du ", label: "Poisson Acrobate", lettreFont: 'p', saison: "Automne", darkness: 1 }, + { key: "araignee", article: "de l'", label: "Araignée", lettreFont: 'a', saison: "Hiver", darkness: 1 }, + { key: "roseau", article: "du ", label: "Roseau", lettreFont: 'r', saison: "Hiver", darkness: 1 }, + { key: "chateaudormant", article: "du ", label: "Château Dormant", lettreFont: 'c', saison: "Hiver", darkness: 1 }, ] const FORMULES_DUREE = [ @@ -64,6 +64,7 @@ export class RdDTimestamp { DEFINITION_HEURES[i].hh = RdDTimestamp.hh(i); DEFINITION_HEURES[i].icon = RdDTimestamp.iconeHeure(i); DEFINITION_HEURES[i].webp = DEFINITION_HEURES[i].icon.replace(".svg", ".webp"); + DEFINITION_HEURES[i].avecArticle = DEFINITION_HEURES[i].article + DEFINITION_HEURES[i].label } } @@ -241,7 +242,7 @@ export class RdDTimestamp { get darkness() { const darknessDebut = 100 * RdDTimestamp.definition(this.heure).darkness - const darknessFin = 100 * RdDTimestamp.definition(this.heure + 1).darkness + const darknessFin = 100 * RdDTimestamp.definition(this.heure + 1).darkness const darknessMinute = Math.round((darknessFin - darknessDebut) * this.minute / RDD_MINUTES_PAR_HEURES); return (darknessDebut + darknessMinute) / 100 } diff --git a/styles/img/ui/scriptarium.svg b/styles/img/ui/scriptarium.svg new file mode 100644 index 00000000..9d066697 --- /dev/null +++ b/styles/img/ui/scriptarium.svg @@ -0,0 +1,65 @@ + + diff --git a/styles/simple.css b/styles/simple.css index da09b65a..77a3b277 100644 --- a/styles/simple.css +++ b/styles/simple.css @@ -572,6 +572,15 @@ input:is(.blessure-premiers_soins, .blessure-soins_complets) { border: none; padding: 0.1rem; } +.context-menu-img { + max-width: 2rem; + max-height: 1rem; + flex-grow: 0; + margin: 0.2rem 0.3rem 0 0; + vertical-align: middle; + border: none; + padding: 0rem; +} .button-img { vertical-align: baseline; diff --git a/templates/settings/options-avancees.hbs b/templates/settings/options-avancees.hbs new file mode 100644 index 00000000..6ea970cd --- /dev/null +++ b/templates/settings/options-avancees.hbs @@ -0,0 +1,13 @@ +
\ No newline at end of file