-
-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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.
If you wish to reuse code for your custom provider, these are the (currently) available Provider modules:
// Get the PDF Provider
import { pdfProvider } from 'https://<hostname of your instance>/modules/actor-export/scripts/lib/providers/PDFProvider.js';
// Get the Scribe Provider
import { scribeProvider } from 'https://<hostname of your instance>/modules/actor-export/scripts/lib/providers/ScribeProvider.js';
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
At the end of your module
, you need to export the Provider object so it can be interpreted by actor-export
:
export { mapper };
The following tools are available to help you. Helper tools can be found in the tools
folder of the repository
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% |
---|
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 };
// 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 };