Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

[Script,Snippet] OGL Spells Ability -- Creates a Spells Token Action (and Ability) which outputs buttons for all of a selected token's spells.

1537237544

Edited 1537237572
The Aaron
Pro
API Scripter
Note: This is specific to the OGL sheet. This script runs when the API starts up and creates an ability on every character that has spells.  The ability will issue a menu by level of available spells (with number of spell slots) as buttons that activate the spell.  Feature Notes: It respects the whisper setting for the character. These abilities are static and do not require the API, so the script can be disabled after generating. Uses the default template Ideally suited for taking characters into games without the API, or setting up Monsters in a Module, or just as a fast way to have spells without digging into the character sheet. For the OGL sheet. Script: on('ready',()=>{ const range = (n) => [...Array(n).keys()]; const spellAbilityName = 'Spells'; const spellProps = ['cantrip','1','2','3','4','5','6','7','8','9']; const spellLevelNames = { cantrip: "Cantrips", 1: "1st Level", 2: "2nd Level", 3: "3rd Level", 4: "4th Level", 5: "5th Level", 6: "6th Level", 7: "7th Level", 8: "8th Level", 9: "9th Level" }; const isSpellSlot = /^lvl(\d)_slots_total$/; const isSpell = /^repeating_spell-([^_]*)_.*_spellname/; const makeButton = (l,n) => `[@{selected|repeating_spell-${l}_$${n}_spellname}](~selected|repeating_spell-${l}_$${n}_spell)`; const makeLevel = (s,l,n) => `{{${spellLevelNames[l]}${s?` (${s} slot${s!==1?'s':''})`:''}=${range(n).map(nn=>makeButton(l,nn)).join('')}}}`; // eslint-disable-line no-irregular-whitespace const makeSpellSheet = (c) => { const counts = {}; const slots = {}; // find all spells findObjs({ type: 'attribute', characterid: c.id }) .forEach(a=>{ let n = a.get('name'); if(isSpellSlot.test(n)){ let m = n.match(isSpellSlot); slots[m[1]]=parseInt(a.get('current')); } else if(isSpell.test(n)){ let m = n.match(isSpell); counts[m[1]]=(counts[m[1]]||0)+1; } }); let parts = []; if(Object.keys(counts).length){ spellProps.forEach(k=>{ if(counts.hasOwnProperty(k)){ parts.push(makeLevel(slots[k],k,counts[k])); } }); let ability = (findObjs({ type: 'ability', characterid: c.id, name: spellAbilityName })[0] || createObj('ability',{ characterid: c.id, name: spellAbilityName })); ability.set({ action: `@{selected|wtype}&{template:default}{{name=@{selected|character_name} Spells}}${parts.join('')}`, istokenaction: true }); } }; let queue = findObjs({ type: 'character' }); const burndownQueue = () => { if(queue.length){ makeSpellSheet(queue.shift()); setTimeout(burndownQueue,0); } else { log('Done Updating Spells Ability'); } }; log(`Considering ${spellAbilityName} Ability on ${queue.length} character${queue.length!==1?'s':''}`); setTimeout(burndownQueue,100); }); Support my work on If you use my scripts, want to contribute, and have the spare bucks to do so , go right ahead. However, please don't feel like you must contribute just to use them! I'd much rather have happy Roll20 users armed with my scripts than people not using them out of some sense of shame. Use them and be happy, completely guilt-free! Disclaimer: This Patreon campaign is not affiliated with Roll20; as such, contributions are voluntary and Roll20 cannot provide support or refunds for contributions.
1537243954
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
To paraphrase Zaphod Beeblebrox, you are not just amazing. You are amazingly amazing!
1537269229

Edited 1537269444
GM Michael
API Scripter
Does this by chance get around the situations when OGL spells break and don't push to chat like they're supposed to (forcing the user to switch it to a spell attack and back or delete and recreate it)?
1537269482
The Aaron
Pro
API Scripter
Probably not, this just yields a collection of buttons which act like clicking on the spells name in the spell list.  I’m not aware of that problem (Full disclosure: I don’t use the OGL sheet and wrote this for a friend.), but I might be able to script a fix for it?
PM'd.  In any event, this is great script, thanks!
The Aaron said: Probably not, this just yields a collection of buttons which act like clicking on the spells name in the spell list.  I’m not aware of that problem (Full disclosure: I don’t use the OGL sheet and wrote this for a friend.), but I might be able to script a fix for it? Primarily I have seen this bug occur when a spellcasting character has been added to the Character Vault and transported into another game. Spells that are set to attack spells save the attack ID as an attribute on the spell. When the character is transported into another game via the Character Vault, the row IDs for the attacks are reset, but the spell attribute that links to the specific attack is not updated. So then when a user tries to use one of these spells, the attack it is referencing doesn't exist, which throws the error.
1537275490
The Aaron
Pro
API Scripter
AH!  I remember tracking that down in the past. Very annoying.  I can probably fix that, let me see if I can duplicate it...
1537325669

Edited 1537328879
GM Michael
API Scripter
So a couple things I'm noticing upon actually using it that are of note. The menu is global, meaning you can't stick this on NPCs without revealing what they can do. This displays both prepared and unprepared spells.  Not really a problem exactly, but it would be nice if there was a setting somewhere. This function goes by whoever is selected, not by who it was created for.  Maybe there's no good way to correct for that, but it can lead to a few weird accidental DM mixups.  Again, not a big deal if you restrict who can see what though. UPDATE : To fix 1 and make 3 a non-issue, I made some minor changes.  It now just whispers to that character. on('ready',()=>{ const range = (n) => [...Array(n).keys()]; const spellAbilityName = 'Spells'; const spellProps = ['cantrip','1','2','3','4','5','6','7','8','9']; const spellLevelNames = { cantrip: "Cantrips", 1: "1st Level", 2: "2nd Level", 3: "3rd Level", 4: "4th Level", 5: "5th Level", 6: "6th Level", 7: "7th Level", 8: "8th Level", 9: "9th Level" }; const isSpellSlot = /^lvl(\d)_slots_total$/; const isSpell = /^repeating_spell-([^_]*)_.*_spellname/; const makeButton = (l,n) => `[@{selected|repeating_spell-${l}_$${n}_spellname}](~selected|repeating_spell-${l}_$${n}_spell)`; const makeLevel = (s,l,n) => `{{${spellLevelNames[l]}${s?` (${s} slot${s!==1?'s':''})`:''}=${range(n).map(nn=>makeButton(l,nn)).join('')}}}`; // eslint-disable-line no-irregular-whitespace const makeSpellSheet = (c) => { const counts = {}; const slots = {}; // find all spells findObjs({ type: 'attribute', characterid: c.id }) .forEach(a=>{ let n = a.get('name'); if(isSpellSlot.test(n)){ let m = n.match(isSpellSlot); slots[m[1]]=parseInt(a.get('current')); } else if(isSpell.test(n)){ let m = n.match(isSpell); counts[m[1]]=(counts[m[1]]||0)+1; } }); let parts = []; let cn = getAttrByName(c.id, 'character_name'); if(!cn) cn = getAttrByName(c.id, 'npc_name'); if(Object.keys(counts).length){ spellProps.forEach(k=>{ if(counts.hasOwnProperty(k)){ parts.push(makeLevel(slots[k],k,counts[k])); } }); let ability = (findObjs({ type: 'ability', characterid: c.id, name: spellAbilityName })[0] || createObj('ability',{ characterid: c.id, name: spellAbilityName })); ability.set({ action: `/w "${cn}" @{selected|wtype}&{template:default}{{name=${cn} Spells}}${parts.join('')}`, istokenaction: true }); } }; let queue = findObjs({ type: 'character' }); const burndownQueue = () => { if(queue.length){ makeSpellSheet(queue.shift()); setTimeout(burndownQueue,0); } else { log('Done Updating Spells Ability'); } }; log(`Considering ${spellAbilityName} Ability on ${queue.length} character${queue.length!==1?'s':''}`); setTimeout(burndownQueue,100); });
This morning, I worked on this further, trying to make one that only did prepared spells, but I haven't been able to figure out how to tell if a character is an npc or not (npcs should always have everything and cantrips should always show up) I tried the following, but I couldn't figure out what I'm missing... log('a:' + c.get('npc')); log('b:' + c.get('attr_npc')); log('c:' + c.get('NPC')); log('d:' + c.get('_npc')); log('e:' + c.get('isNpc')); I really thought c.get('npc') would do it since @{selected|npc} works, so it's probably something obvious that I'm missing because it's 6AM and I'm sick...
1537358630
The Aaron
Pro
API Scripter
Michael G. said: So a couple things I'm noticing upon actually using it that are of note. The menu is global, meaning you can't stick this on NPCs without revealing what they can do. This displays both prepared and unprepared spells.  Not really a problem exactly, but it would be nice if there was a setting somewhere. This function goes by whoever is selected, not by who it was created for.  Maybe there's no good way to correct for that, but it can lead to a few weird accidental DM mixups.  Again, not a big deal if you restrict who can see what though. UPDATE : To fix 1 and make 3 a non-issue, I made some minor changes.  It now just whispers to that character. 1) This is handled by the whisper setting of the token.  @{selected|wtype} will expand to a whisper command if the token is set to whisper. 2) This could be addressed, I'll look into it.  This is largely intended for NPCs, I would expect PCs to change too often for this to be overly useful, but maybe having a command to regenerate the ability would fix that. 3) This could be switched out to be explicit to the character, probably a good idea.  The original concept of operations was to select a token, click the token action, then click the spell.  I suppose if you wanted to add it to a macro bar, you wouldn't necessarily have the token selected.  I'll look at that. Michael G. said: I really thought c.get('npc') would do it since @{selected|npc} works, so it's probably something obvious that I'm missing because it's 6AM and I'm sick... You'd need to get the npc attribute for that character.  It won't show up as a property of the character object.
Ah, that's what that does... Yeah, on my end, I made a regenerate command.  Obviously wouldn't work without the API, but eh.  For my players with machines one could mistake for a potato though, a chat menu is vastly better than the character sheet.
1537359293

Edited 1537359336
The Aaron
Pro
API Scripter
I think there is already an API script that dynamically builds menus for player spells.&nbsp; Kevin wrote it a few years ago, IIRC.&nbsp;&nbsp; I think this is it:&nbsp;<a href="https://app.roll20.net/forum/post/5608775/script-update-tokenaction-creator-for-5e-ogl-sheet-version-2-dot-0/?pagenum=1" rel="nofollow">https://app.roll20.net/forum/post/5608775/script-update-tokenaction-creator-for-5e-ogl-sheet-version-2-dot-0/?pagenum=1</a>