Skip to content

Custom Providers

William edited this page May 3, 2024 · 9 revisions

Custom Providers

Custom providers are simple to create if you know the lay of the land, that is how the actor-export libraries work and how the Foundry VTT actor object is defined.

imports

For starters, you need to import any and all JavaScript modules you want to use. These need to include the full path to the JavaScript files, due to how dynamic imports work in JavaScript.

actor-export will provide your code as an Encoded URI Component. Because of this, there is no base working URI.

Any Provider class you write must extend the baseProvider class. This is validated when executed.

Existing Provider class

If you wish to reuse code for your custom provider, these are the (currently) available Provider modules:

PDF Provider class

// Get the PDF Provider
import { pdfProvider } from 'https://<hostname of your instance>/modules/actor-export/scripts/lib/providers/PDFProvider.js';

Scribe Provider classes

// Get the Scribe Provider
import { scribeProvider } from 'https://<hostname of your instance>/modules/actor-export/scripts/lib/providers/ScribeProvider.js';

JavaScript code

After the imports you need to add the JavaScript code which will perform all the heavy lifting. If you are using the built-in providers, please check the relevant modules (files) for documentation of how to use the classes and functions. Certain Foundry VTT objects are available for you:

  • game object
  • actor object
const mapper = pdfProvider(actor);
/* or */
const mapper = scribeProvider(actor);

/* Add code here

exports

At the end of your module, you need to export the Provider object so it can be interpreted by actor-export:

export { mapper };

Helper tools

The following tools are available to help you. Helper tools can be found in the tools folder of the repository

convert-pdf-export.py

tools/convert-pdf-export.py

This tool allows you to convert existing pdf-export mappings to actor-export format.

Usage:

tools/convert-pdf-export.py /path/to/mapping/file.mapping

This python tool only needs one argument, the full path to your mapping file, and will print a custom provider to be used in the custom provider dialog. PDF files can only be uploaded through the actor export dialog.

⚠️ Your mileage may vary depending on the quality of the mappings file. The tool performs some basic cleanup, but since it is a basic text parser, some mapping files may not work 100%

Examples

Conversion of a pdf-export mappings file

tools/convert-pdf-export.py DnD35eSheet.mapping                                                                                                                                                             17:33:53
import { pdfProvider } from 'https://<ENTER FOUNDRY VTT HOSTNAME>/modules/actor-export/scripts/lib/providers/PDFProvider.js';
const mapper = new pdfProvider(actor);
/* This is a very basic mapper for PDF exports */
/**
* mapper.field syntax:
* mapper.field(filename, field_name, value)
*   filename: use 'all', as in the case of custom Providers you cannot specify multiple files
*   field_name: the name of the form field in the PDF file (no, at this time I cannot provide you with a list)
*   value: the value of the PDF form field. If you are targetting a chackbox, make sure the value is either true or false
*/

mapper.field('all','CharacterName', actor.name);
mapper.field('all','PlayerName', Object.entries(actor.ownership).filter(entry => entry[ 1 ] === 3).map(entry => entry[ 0 ]).map(id => !game.users.get(id)?.isGM ? game.users.get(id)?.name : null).filter(x => x).join(", "));
mapper.field('all','CurrentHP', actor.system.attributes.hp.value);
mapper.field('all','MaxHP', actor.system.attributes.hp.max);
mapper.field('all','ClassString', actor.items.filter(i => i.type === 'class').map(i => `${i.name} ${i.system.levels}`).join(' / '));
mapper.field('all','Level', actor.system.details.level.value);
mapper.field('all','RaceandTemplate', actor.items.filter(i => i.type === 'race' || i.type === 'template').map(i => `${i.name}`).join(' / '));
mapper.field('all','Alignment', actor.system.details.alignment);
mapper.field('all','Height', actor.system.details.height);
mapper.field('all','Weight', actor.system.details.weight);
mapper.field('all','Size', actor.system.traits.size);
mapper.field('all','Gender', actor.system.details.gender);
mapper.field('all','Deity', actor.system.details.deity);
mapper.field('all','STR', actor.system.abilities.str.total);
mapper.field('all','DEX', actor.system.abilities.dex.total);
mapper.field('all','CON', actor.system.abilities.con.total);
mapper.field('all','INT', actor.system.abilities.int.total);
mapper.field('all','WIS', actor.system.abilities.wis.total);
mapper.field('all','CHA', actor.system.abilities.cha.total);
mapper.field('all','BaseSTR', actor.system.abilities.str.value);
mapper.field('all','BaseDEX', actor.system.abilities.dex.value);
mapper.field('all','BaseCON', actor.system.abilities.con.value);
mapper.field('all','BaseINT', actor.system.abilities.int.value);
mapper.field('all','BaseWIS', actor.system.abilities.wis.value);
mapper.field('all','BaseCHA', actor.system.abilities.cha.value);
mapper.field('all','EnhanceModSTR', actor.system.abilities.str.total - actor.system.abilities.str.value);
mapper.field('all','EnhanceModDEX', actor.system.abilities.dex.total - actor.system.abilities.dex.value);
mapper.field('all','EnhanceModCON', actor.system.abilities.con.total - actor.system.abilities.con.value);
mapper.field('all','EnhanceModINT', actor.system.abilities.int.total - actor.system.abilities.int.value);
mapper.field('all','EnhanceModWIS', actor.system.abilities.wis.total - actor.system.abilities.wis.value);
mapper.field('all','EnhanceModCHA', actor.system.abilities.cha.total - actor.system.abilities.cha.value);
mapper.field('all','MiscModSTR', 0);
mapper.field('all','MiscModDEX', 0);
mapper.field('all','MiscModCON', 0);
mapper.field('all','MiscModINT', 0);
mapper.field('all','MiscModWIS', 0);
mapper.field('all','MiscModCHA', 0);
mapper.field('all','MiscPenSTR', actor.system.abilities.str.damage + actor.system.abilities.str.drain + actor.system.abilities.str.penalty + actor.system.abilities.str.userPenalty);
mapper.field('all','MiscPenDEX', actor.system.abilities.dex.damage + actor.system.abilities.dex.drain + actor.system.abilities.dex.penalty + actor.system.abilities.dex.userPenalty);
mapper.field('all','MiscPenCON', actor.system.abilities.con.damage + actor.system.abilities.con.drain + actor.system.abilities.con.penalty + actor.system.abilities.con.userPenalty);
mapper.field('all','MiscPenINT', actor.system.abilities.int.damage + actor.system.abilities.int.drain + actor.system.abilities.int.penalty + actor.system.abilities.int.userPenalty);
mapper.field('all','MiscPenWIS', actor.system.abilities.wis.damage + actor.system.abilities.wis.drain + actor.system.abilities.wis.penalty + actor.system.abilities.wis.userPenalty);
mapper.field('all','MiscPenCHA', actor.system.abilities.cha.damage + actor.system.abilities.cha.drain + actor.system.abilities.cha.penalty + actor.system.abilities.cha.userPenalty);
mapper.field('all','ModSTR', actor.system.abilities.str.mod);
mapper.field('all','ModDEX', actor.system.abilities.dex.mod);
mapper.field('all','ModCON', actor.system.abilities.con.mod);
mapper.field('all','ModINT', actor.system.abilities.int.mod);
mapper.field('all','ModWIS', actor.system.abilities.wis.mod);
mapper.field('all','ModCHA', actor.system.abilities.cha.mod);
mapper.field('all','BAB', actor.system.attributes.bab.total);
mapper.field('all','AttackName', actor.items.filter(i => i.type === 'attack').map(i => `${i.name}`).join('\n'));
mapper.field('all','AttackBonus', actor.items.filter(i => i.type === 'attack').map(i => `${i.system.attackBonus}`).join('\n'));
mapper.field('all','DamageFormula', actor.items.filter(i => i.type === 'attack').map(i => `${i.system.damage.parts[0][0].replace(/sizeRoll\((?<count>[0-9]*), ?(?<size>[0-9]*), ?\u0040size\)/, "$<count>d$<size>")}`).join('\n'));
mapper.field('all','AttackCritRange', actor.items.filter(i => i.type === 'attack').map(i => `${i.system.ability.critRange}-20`).join('\n'));
mapper.field('all','AttackCritMultiplier', actor.items.filter(i => i.type === 'attack').map(i => `x${i.system.ability.critMult}`).join('\n'));
mapper.field('all','AttackAbilityScore', actor.items.filter(i => i.type === 'attack').map(i => `${i.system.ability.attack}`).join('\n'));
mapper.field('all','AttackRange', actor.items.filter(i => i.type === 'attack').map(i => `${i.system.range.value} ${i.system.range.units}`).join('\n'));
mapper.field('all','SkillName0', 'Appraisal');
mapper.field('all','ClassSkill0', actor.system.skills.apr.cs);
mapper.field('all','SkillRanks0', actor.system.skills.apr.rank);
mapper.field('all','SkillTotalMod0', actor.system.skills.apr.mod);
mapper.field('all','SkillName1', 'Auto-Hypnosis');
mapper.field('all','ClassSkill1', actor.system.skills.aut.cs);
mapper.field('all','SkillRanks1', actor.system.skills.aut.rank);
mapper.field('all','SkillTotalMod1', actor.system.skills.aut.mod);
mapper.field('all','SkillName2', 'Balance');
mapper.field('all','ClassSkill2', actor.system.skills.blc.cs);
mapper.field('all','SkillRanks2', actor.system.skills.blc.rank);
mapper.field('all','SkillTotalMod2', actor.system.skills.blc.mod);
mapper.field('all','SkillName3', 'Bluff');
mapper.field('all','ClassSkill3', actor.system.skills.blf.cs);
mapper.field('all','SkillRanks3', actor.system.skills.blf.rank);
mapper.field('all','SkillTotalMod3', actor.system.skills.blf.mod);
mapper.field('all','SkillTotalMod99', (actor.system.skills.crf.subSkills.crf1?.name) ? actor.system.skills.crf.subSkills.crf1.mod : "");
mapper.field('all','Abilities', actor.items.filter(i => i.type === 'feat' && i.system.source != '').map(i => `${i.name}`).join('\n'));
mapper.field('all','AbilitiesSource', actor.items.filter(i => i.type === 'feat' && i.system.source != '').map(i => `${i.system.source}`).join('\n'));
mapper.field('all','FeatName', actor.items.filter(i => i.type === 'feat' && i.system.source === '').map(i => `${i.name}`).join('\n'));
mapper.field('all','FeatSource', actor.items.filter(i => i.type === 'feat' && i.system.source === '').map(i => `${i.system.classSource}`).join('\n'));
mapper.field('all','Weapons', actor.items.filter(i => i.type === 'weapon').map(i => `${i.name}`).join('\n'));
mapper.field('all','Armor', actor.items.filter(i => i.type === 'armor').map(i => `${i.name}`).join('\n'));
mapper.field('all','Equipment', actor.items.filter(i => i.type === 'equipment').map(i => `${i.name}`).join('\n'));
mapper.field('all','Consumables', actor.items.filter(i => i.type === 'consumable').map(i => `${i.name}`).join('\n'));
mapper.field('all','Loot', actor.items.filter(i => i.type === 'loot').map(i => `${i.name}`).join('\n'));
mapper.field('all','BuffName', actor.items.filter(i => i.type === 'buff').map(i => `${i.name}`).join('\n'));
mapper.field('all','BuffType', actor.items.filter(i => i.type === 'buff').map(i => `${i.system.buffType}`).join('\n'));
mapper.field('all','BuffLevel', actor.items.filter(i => i.type === 'buff').map(i => `${i.system.level}`).join('\n'));
mapper.field('all','BuffActive', actor.items.filter(i => i.type === 'buff').map(i => `${i.system.active}`).join('\n'));
export { mapper };

Export an actor's PF2e spell list

// import the relevant modules and objects
import { scribeProvider } from 'https://foundry.elaba.net/modules/actor-export/scripts/lib/providers/ScribeProvider.js';
import { pf2eHelper } from 'https://foundry.elaba.net/modules/actor-export/scripts/lib/helpers/PF2eHelper.js';

// Create the Provider object
const mapper = new scribeProvider(actor);
/* by default there is no destination filename, as it is supposed to be
 * in the sheet.json file, which accompanies regular provider definitions
 * overriding destinationFileName and sourceFileURI is REQUIRED!
 * in the case of scribeProvider
 */
mapper.overrideDestinationFileName = 'spellist.scribe';
mapper.overrideFilePath = 'spellist.scribe';

/* Do the heaving lifing */
const spells = actor.items
    .filter((i) => i.type === 'spell')
    .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));

const headerCells = ['Spell', 'actions', 'Defense', 'rank', 'range', 'AofE'];
const spellTable = new scribeProvider.class.scribeTableEntry('Spell List', headerCells);
actor.items
    .filter((i) => i.type === 'spell')
    .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
    .forEach((el) => {
    const activity = pf2eHelper.formatSpellCastingTime(el.system.time.value, pf2eHelper.scribeActivityGlyphs);
    let defense = '';
    if (el.system.defense?.passive !== undefined) {
        defense = el.system.defense.passive.statistic;
    } else if (el.system.defense?.save !== undefined) {
        defense =
            (el.system.defense.save.basic ? 'Basic ' : '') + pf2eHelper.capitalize(el.system.defense.save.statistic);
    }
    const spellType = el.isCantrip ? 'Cantrip' : el.isFocusSpell ? 'Focus' : 'Spell';
    const rank = `${spellType} ${el.rank}`;
    const range = el.system.range?.value || '';
    let AofE = '';
    if (el.system.area !== null) {
        AofE = `${el.system.area.value}ft ${el.system.area.type}`;
    } else {
        AofE = el.system.target?.value || '';
    }
    spellTable.addContentRow([el.name, activity, defense, rank, range, AofE]);
});

mapper.scribe(mapper.overrideFilePath, spellTable.scribify());

// Export the mapper object
export { mapper };