Finderski said: And for further clarification...given that no custom compendium is available, that does mean, while the workaround is great for fully stated characters, it does NOT give you the ability to drag and drop items (e.g. weapons or other gear) onto a character using Transmogrifier. For me, the power of a compendium is less about the monsters/NPC I can drag and drop, but more about the power of re-usable weapons, spells, etc. that I can drag and drop onto a character sheet. You could create an API script to generate data on the character sheet automatically in similar fashion to a custom compendium. I did this for a collection of custom spells in one of my 5e campaigns. Similar scripts could be made for other parts of the character sheet. on('ready', () => {
let spellData = {};
function initSpellData() {
spellData = {};
const spellHandouts = filterObjs((o) => o.get('type') === 'handout' && o.get('name').indexOf('Custom Spell: ') === 0);
spellHandouts.forEach((spell) => {
spell.get('gmnotes', (notes) => {
notes = notes.replace(/<.*?>/g, '').replace(/(&nbsp;|\s)+/g, ' ');
try {
notes = JSON.parse(notes);
} catch {
log(` ### JSON PARSE ERROR: Skipping ${spell.get('name')}`);
log(notes);
}
spellData[spell.get('name').substring(14)] = notes;
});
});
}
initSpellData();
const generateUUID = (() => {
let a = 0;
let b = [];
return () => {
let c = (new Date()).getTime() + 0;
let f = 7;
let e = new Array(8);
let d = c === a;
a = c;
for (; 0 <= f; f--) {
e[f] = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(c % 64);
c = Math.floor(c / 64);
}
c = e.join("");
if (d) {
for (f = 11; 0 <= f && 63 === b[f]; f--) {
b[f] = 0;
}
b[f]++;
} else {
for (f = 0; 12 > f; f++) {
b[f] = Math.floor(64 * Math.random());
}
}
for (f = 0; 12 > f; f++) {
c += "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz".charAt(b[f]);
}
return c;
};
})();
function generateRowID() {
return generateUUID().replace(/_/g, "Z");
}
function createAttrs(prefix, characterid, attrs) {
const createdObjs = [];
Object.keys(attrs).forEach((name) => {
const newObj = createObj('attribute', {
name: prefix + name,
current: '',
characterid: characterid,
});
newObj.setWithWorker({current: attrs[name]});
createdObjs.push(newObj);
});
return createdObjs;
}
on('chat:message', (msg) => {
if (msg.type !== 'api') return;
if (msg.content.indexOf('!custom-spell') !== 0) return;
const args = msg.content.splitArgs();
args.shift();
const fromWho = getObj('player', msg.playerid);
const fromName = fromWho.get('displayname');
if (args[0] === 'refresh' && playerIsGM(msg.playerid)) {
// spellData is loaded when the sandbox spins up, but `!custom-spell refresh` lets a GM update spellData without restarting the API.
initSpellData();
sendChat('System', '/w gm Custom spell data refreshed!', null, {noarchive: true});
return;
}
if (args[0] === 'debug' && playerIsGM(msg.playerid)) {
const spellNames = Object.keys(spellData).filter((name) => name.toLowerCase().indexOf((args[1] || '').toLowerCase()) >= 0);
const spell = spellData[spellNames[0]];
if (!spell) {
sendChat('System', `/w gm No spell found for &lt;${args[1] || ''}&gt;`, null, {noarchive:true});
return;
}
sendChat('System', `/w gm &{template:default} {{name=${spellNames[0]}}} {{=<pre><code>${JSON.stringify(spell, null, 2)}</code></pre>}}`, null, {noarchive:true});
return;
}
if (args[0] === 'search') {
// produces a list of custom spells, potentially fileterd by a search term
let spellNames = Object.keys(spellData);
let output = '&{template:default}';
if (args[1]) {
spellNames = spellNames.filter((name) => name.toLowerCase().indexOf(args[1].toLowerCase()) >= 0);
output += `{{name=Custom Spells matching &lt;${args[1]}&gt;}}`;
} else {
output += '{{name=Custom Spells}}';
}
output += spellNames.map((name, i) => `{{${''.padStart(i, ' ')}=[${name}](!custom-spell add &#34;${name}&#34;)}}`).join('');
sendChat('System', `/w "${fromName}" ${output}`, null, {noarchive:true});
return;
}
if (args[0] === 'add') {
// adds a specified custom spell to the selected character
if (!args[1]) {
sendChat('System', `/w "${fromName}" No custom spell specified`, null, {noarchive: true});
return;
}
if (!spellData[args[1]]) {
sendChat('System', `/w "${fromName}" No custom spell named &lt;${args[1]}&gt; found. Try using [search](!custom-spell search) to get an exact name.`, null, {noarchive:true});
return;
}
if (!msg.selected) {
sendChat('System', `/w "${fromName}" No character selected. Please select a character and try again.`, null, {noarchive:true});
return;
}
const selected = getObj(msg.selected[0]._type, msg.selected[0]._id);
if (!selected.get('represents')) {
sendChat('System', `/w "${fromName}" Selected token has no associated character. Please select a token that is linked to a character sheet.`, null, {noarchive:true});
return;
}
const character = getObj('character', selected.get('represents'));
const spell = spellData[args[1]];
const rowId = generateRowID();
const attrPrefix = `repeating_spell-${spell.spelllevel}_${rowId}_`;
const createdAttrs = createAttrs(attrPrefix, character.id, {
spellname: args[1],
'options-flag': 'false',
'details-flag': 'false',
spellschool: spell.spellschool,
spellritual: spell.spellritual ? '{{ritual=1}}' : '0',
spellcastingtime: spell.spellcastingtime,
spellrange: spell.spellrange,
spelltarget: spell.spelltarget,
spellcomp_v: spell.components.spellcomp_v ? '{{v=1}}' : '0',
spellcomp_s: spell.components.spellcomp_s ? '{{s=1}}' : '0',
spellcomp_m: spell.components.spellcomp_m ? '{{m=1}}' : '0',
spellcomp_materials: spell.components.spellcomp_materials,
spellconcentration: spell.spellconcentration ? '{{concentration=1}}' : '0',
spellduration: spell.spellduration,
spell_ability: 'spell',
innate: '',
// spelloutput: spell.output.spelloutput,
spelloutput: 'SPELLCARD',
spellattack: spell.output.spellattack,
spelldamage: spell.output.spelldamage,
spelldamagetype: spell.output.spelldamagetype,
spelldamage2: spell.output.spelldamage2,
spelldamagetype2: spell.output.spelldamagetype2,
spellhealing: spell.output.spellhealing,
spelldmgmod: spell.output.spelldmgmod ? 'Yes' : '0',
spellsave: spell.output.spellsave,
spellsavesuccess: spell.output.spellsavesuccess,
spellhldie: spell.output.spellhldie,
spellhldietype: spell.output.spellhldietype,
spellhlbonus: spell.output.spellhlbonus,
includedesc: spell.output.includedesc,
spelldescription: spell.output.spelldescription,
spellathigherlevels: spell.output.spellathigherlevels,
});
return;
}
});
}); Each spell in the "compendium" is specified with its own handout (which can be archived), and the GM notes field is filled with JSON containing all the spell details, like this: {
"spelllevel": 3,
"spellschool": "abjuration",
"spellritual": false,
"spellcastingtime": "1 action",
"spellrange": "self",
"spelltarget": "self",
"components": {
"spellcomp_v": true,
"spellcomp_s": true,
"spellcomp_m": false,
"spellcomp_materials": ""
},
"spellconcentration": false,
"spellduration": "1 minute",
"output": {
"spellattack": "None",
"spelldamage": "",
"spelldamagetype": "",
"spelldamage2": "",
"spelldamagetype2": "",
"spellhealing": "",
"spelldmgmod": false,
"spellsave": "",
"spellsavesuccess": "",
"spellhldie": "",
"spellhldietype": "",
"spellhlbonus": "",
"includedesc": "off",
"spelldescription": "This spell creates an invisible magic field that does not stop weapons from moving toward you, but impedes their motion when they are retracted. When you are hit with a melee attack, make a contested ability check using your spellcasting ability against the attacker's ability score used in the attack. If you succeed, your opponent's attacking weapon or body part becomes stuck in the field.\n\nIf your opponent's melee weapon becomes stuck in the field, the opponent may release the weapon and move away from you. If your opponent attacked with an unarmed strike, natural weapon, or refuses to release their weapon, they are grappled by you. You do not require a free hand to grapple the creature, but only one creature can be grappled via *blade snare* at a time; if another creature hits you with a melee attack, you must release the first one in order to attempt to grapple the second.\n\nAs an action, a creature grappled in this way may make a Strength (Athletics) or Dexterity (Acrobatics) check with a DC equal to your spell save DC, ending the grapple and escaping the field on success.\n\nWhile *blade snare* is active, you may not make ranged weapon attacks, but you can make ranged spell attacks and melee attacks freely.",
"spellathigherlevels": ""
}
} The !custom-spell search command lets the users search/filter the list of custom spells, with buttons to add a spell to the character sheet of the currently selected token.