First, thanks to Actoba for all of his hard work on the DnD5e Character sheet. My players and I really appreciate it. Currently you either have to click the "Cast Spell" button on the character sheet, or you have to hard code a macro with what the output of the button is (meaning that any changes you make to the spell definition require updating the macro). So I have started writing this API to allow emulating what appears to happen when the button on the character sheet is clicked. It is obviously very dependent on the character sheet, so any changes to the character sheet might require some updates to this script. It also does a good bit of variable replacement when necessary. I have tried to cover the variables that I could find had issues, but it is possible that you might come across one that causes trouble. I welcome any feedback, or a better way to accomplish this... // Depends on splitArgs.js (<a href="https://github.com/Roll20/roll20-api-scripts/tree/master/splitArgs" rel="nofollow">https://github.com/Roll20/roll20-api-scripts/tree/master/splitArgs</a>) /***** Helpful Macros ***** * Cast-Spell = !DnD5e_CastSpell @{selected|character_id} ?{Level (0 = Cantrip)|0} ?{Row|1} @{target|token_id} *****/ /***** Usage ***** * !DnD5e_CastSpell @{[Your Character Full Name]|character_id} [Spell Level (0 for Cantrip)] [Spell Row in the Spellbook] @{target|token_id} * * Example: * !DnD5e_CastSpell @{Knox Promethean|character_id} 1 8 @{target|token_id} * * This example would make the character "Knox Promethean" cast the spell that is level 1 on row 8, * and it will prompt the user to click on a target. It just gets the target for use. You still need * to specify how you are using it inside your spell. For example, in the "Target/Area of Effect" box * you might put "@{target|token_name}". This will make it show your targets name in the info block when * it shows the spell cast (if you have the Info block checkbox checked for the spell). * * You can use "selected" in lieu of [Your Character Full Name], but you will have to have a token selected * the entire time. * * If you do not need a target, then remove the "@{target|token_id}" from the end of the macro. *****/ var APM3_DnD5e_CastSpell = APM3_DnD5e_CastSpell || (function() { 'use strict'; var version = 1.0; var castTemplate = '&{template:5eDefault} {{spell=1}} {{title=@{spellname}}} {{subheader=@{character_name}}} {{subheaderright=@{spellschool} @{spellfriendlylevel}}} {{subheader2=@{spellconcentration} @{spellritual}}} @{spellcastmacrooptions} {{spellcasttime=@{spellcasttime}}} {{spellduration=@{spellduration}}} {{spelltarget=@{spelltarget}}} {{spellrange=@{spellrange}}} {{spellgainedfrom=@{spellgainedfrom}}} {{spellcomponents=@{spellcomponents}}} @{classactionspellcast}'; var spellbookKeys = [ 'attackstat', 'customsavedc', 'damage', 'damagemiscbonus', 'damagestatbonus', 'damagetype', 'healstatbonus', 'savestat', 'savesuccess', 'spellbaselevel', // 'spellcancrit', // 'spellcasttime', 'spellcomponents', 'spellconcentration', 'spelldescription', 'spellduration', 'spelleffect', 'spellgainedfrom', 'spellhealamount', 'spellhighersloteffect', 'spellisprepared', 'spellname', 'spellrange', 'spellritual', 'spellsavedc', 'spellschool', 'spellshowattack', 'spellshowattackadv', 'spellshowdamage', 'spellshowdesc', 'spellshoweffects', 'spellshowhealing', 'spellshowhigherlvl', 'spellshowinfoblock', 'spellshowsavethrow', 'spelltarget', 'spelltypdamage', 'spelltypeadvanced', 'spelltypeattack', 'spelltypeeffects', 'spelltypeheal', 'spelltypesave' ]; var doNotRetrieveKeys = [ 'bard_spell_bonus', 'bard_spell_dc', 'charisma_mod', 'classactionspellcast', 'cleric_spell_bonus', 'cleric_spell_dc', 'constitution_mod', 'dexterity_mod', 'druid_spell_bonus', 'druid_spell_dc', 'global_spell_attack_bonus', 'global_spell_damage_bonus', 'intelligence_mod', 'paladin_spell_bonus', 'paladin_spell_dc', 'PB', 'ranger_spell_bonus', 'ranger_spell_dc', 'sorcerer_spell_bonus', 'sorcerer_spell_dc', 'spellbaselevel', 'strength_mod', 'warlock_spell_bonus', 'warlock_spell_dc', 'wisdom_mod', ]; var keyChanges = { spellcastmacrooptions: '@{spellshowinfoblock} @{spellshowdesc} @{spellshowhigherlvl} @{spellshowattack} @{spellshowattackadv} @{spellshowsavethrow} @{spellshowhealing} @{spellshowdamage} @{spellshoweffects}' }; var defaultValues = { attackstat: '0', customsavedc: '0', damage: '0', damagemiscbonus: '0', damagestatbonus: '0', healstatbonus: '0', savestat: 'STR', spellcancrit: '{{spellcancrit=1}} {{spellcritdamage=Additional [[@{damage}]] damage}}', spellhealamount: '0', spellsavedc: '0' }; var chatCommands = { dnd5e_castspell: function(args, msg) { log('APM3_DnD5e_CastSpell.CastSpell(' + args.toString() + ') called'); var characterId = (args.shift() || ''); if( (characterId === undefined) || (characterId === null) ) { log('CharacterId parameter missing or invalid.'); return; } var character = getObj('character', characterId); if( (character === undefined) || (character === null) ) { log('Unable to find character.'); return; } var level = parseInt(args.shift() || -1); if(level &lt; 0) { log('Level parameter missing or invalid.'); return; } var row = parseInt(args.shift() || -1); if(row &lt; 1) { log('Row parameter missing or invalid.'); return; } --row; var targetId = (args.shift() || ''); var keyCache = { character_id: characterId, character_name: character.get('name'), spellbookKeyPrefix: 'repeating_spellbook' + (level === 0 ? 'cantrip' : 'level' + level) + '_' + row + '_', spellbaselevel: level, spellfriendlylevel: (level === 0 ? 'Cantrip' : 'Level ' + level), target: null, targetId: null, targetCharacterId: null }; if(targetId.length &gt; 0) { var targetCharacterId = targetId; var target = getObj('character', targetId); if( (target === undefined) || (target === null) ) { target = getObj('graphic', targetId); if( (target === undefined) || (target === null) ) { log('Unable to find target: ' + targetId); target = null; targetId = null; targetCharacterId = null; } else { targetCharacterId = target.get('represents'); } } keyCache.targetId = targetId; keyCache.targetCharacterId = targetId; keyCache.target = target; } //logAllCharacterAttributes(characterId, keyCache.spellbookKeyPrefix); var txt = cleanString(keyCache, castTemplate); log('Cleaned Template: ' + txt); sendChat('player|' + msg.playerid, txt); } }; function cleanString(keyCache, src) { if( (src === undefined) || (src === null) ) return ''; var pieces = src.split('@{'); // Skip the first piece, because it cant possibly be a variable for(var i = 1; i &lt; pieces.length; ++i) { var piece = pieces[i]; var idx = piece.indexOf('}'); if(idx &lt; 0) { // This shouldnt happen, but just in case pieces[i] = '@{' + piece; continue; } var key = piece.substring(0, idx); if(key.indexOf('|') &gt;= 0) { // If the variable has a target var keyPieces = key.split('|'); if( (keyPieces[0] === 'target') && (keyCache.targetId !== null) ){ // If the target is for "target" if(startsWith(keyPieces[1], 'character_')) { // If the variable is a character value pieces[i] = piece.replace(key + '}', cleanString(keyCache, getAttrByName(keyCache.targetCharacterId, keyPieces[1]))); } else { if(startsWith(keyPieces[1], 'token_')) { // If the variable is a token value keyPieces[1] = keyPieces[1].replace('token_', ''); // Get rid of the 'token_' prefix } pieces[i] = piece.replace(key + '}', cleanString(keyCache, keyCache.target.get(keyPieces[1]))); } } else if(keyPieces[0] === 'selected') { // If the target is for "selected" pieces[i] = piece.replace(key + '}', cleanString(keyCache, getAttrByName(keyCache.character_id, keyPieces[1]))); } else { // The key has a target, so it must be fully qualified already, right? I hope so. pieces[i] = '@{' + piece; } continue; } var cleanKey = (_.indexOf(spellbookKeys, key, true) &gt;= 0 ? keyCache.spellbookKeyPrefix : '') + key; if(_.has(keyCache, key)) { // Get the value from the cache if its there cleanKey = keyCache[key]; } else { if(_.has(keyChanges, key)) { // Flip the key to something else if needed, and clean it cleanKey = cleanString(keyCache, keyChanges[key]); } else if(_.indexOf(doNotRetrieveKeys, key) &gt;= 0) { // If we shouldnt resolve this key, then dont. cleanKey = '@{' + keyCache.character_name + '|' + cleanKey + '}'; } else { var attr = getAttributeByName(keyCache.character_id, cleanKey); var value = ((attr !== undefined) && (attr !== null)) ? attr.get('current') : null; if( (value === undefined) || (value === null) ) { cleanKey = (_.has(defaultValues, key) ? cleanString(keyCache, defaultValues[key]) : ''); } else { cleanKey = cleanString(keyCache, value); } } keyCache[key] = cleanKey; } pieces[i] = piece.replace(key + '}', cleanKey); }; return pieces.join(''); } function handleChatMessage(msg) { var isApi = msg.type === 'api'; var args = msg.content.trim().splitArgs(); if (isApi) { var command = args.shift().substring(1).toLowerCase(); var arg0 = args.shift() || ''; var isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; if (!isHelp) { if (arg0) { args.unshift(arg0); } if (_.isFunction(chatCommands[command])) { chatCommands[command](args, msg); } } else if (_.isFunction(chatCommands.help)) { chatCommands.help(command, args, msg); } } else if (_.isFunction(chatCommands['msg_' + msg.type])) { chatCommands['msg_' + msg.type](args, msg); } } function onReady() { on('chat:message', handleChatMessage); } function getAttributeByName(characterId, attributeName, defaultCurrent, defaultMax) { var attribute = findObjs({ type: 'attribute', characterid: characterId, name: attributeName })[0]; if ((attribute === null) || (attribute === undefined)) { // If no default is defined, then just return null if (((defaultCurrent === null) || (defaultCurrent === undefined)) && ((defaultMax === null) || (defaultMax === undefined))) return null; // else create it var o = { characterid: characterId, name: attributeName } if ((defaultCurrent !== undefined) && (defaultCurrent !== null)) o.current = defaultCurrent; if ((defaultMax !== undefined) && (defaultMax !== null)) o.max = defaultMax; attribute = createObj('attribute', o); } return attribute; } function logAllCharacterAttributes(characterId, nameStartsWith) { var attributes = findObjs({ type: 'attribute', characterid: characterId }); var cnt = 0; _.each(attributes, function (attribute) { var name = attribute.get('name'); if ((nameStartsWith !== undefined) && (nameStartsWith !== null) && (nameStartsWith.length &gt; 0) && (!startsWith(name, nameStartsWith))) return; log(++cnt + ') ' + JSON.stringify({name: name, current: attribute.get('current'), max: attribute.get('max') })); }); log(cnt + ' attributes found.'); } function replaceAll(find, replace, str) { return str.replace(new RegExp(find, 'g'), replace); } function startsWith(src, find) { return src.lastIndexOf(find, 0) === 0; } return { OnReady: onReady }; }()); on('ready',function() { 'use strict'; APM3_DnD5e_CastSpell.OnReady(); });