
Pathfinder Action Menu.
For the Pathfinder by Roll20 Sheet.
This script generates a chat menu for a character of attacks, spells, spell-like abilities, or any combination of them, and whispers that chat menu so it doesnt clutter up the chat. You must be using the Pathfinder by Roll20 sheet (haven't tested with others, but i expect it won't work).
It can be used by players and GMs.
Here's a good way to call it:
!pfactionmenu @{selected|character_id} ?{Menu Type?|Attacks|Spells|Spell-like|All}
You can also use any of the following to launch it:
!pfactionmenu @{selected|character_id} All
!pfactionmenu @{selected|character_id} Attacks Spell-like
!pfactionmenu @{selected|character_id} Spells
!pfactionmenu @{selected|character_id} Spell-like Spells
The chat menu will leave out any sections you dont have, so if you select All, but dont have any spell-like abilities, or spells above 4th level, those sections won't be printed.
Edit: Update
Sheet now has the option to show only prepared spells. Add the word 'prepared' anywhere after character_id, like so:
!pfactionmenu @{selected|character_id} Attacks Spells prepared !pfactionmenu @{selected|character_id} prepared Spells Spell-like !pfactionmenu @{selected|character_id} prepared Spells
It uses the pf rolltemplate and roll20's standard API menu buttoms, so its output looks like this
Paste the following into your scripts page:
/* Pathfinder !pfactionmenu @{selected|character_id} ?{Menu Type?|Attacks|Spells|Spell-like|All} !pfactionmenu @{target|character_id} Attacks !pfactionmenu @{name|character_id} Spells !pfactionmenu @{selected|character_id} Attacks Spell-like !pfactionmenu @{selected|character_id} All if you want to show only prepared spells, add 'prepared' anywhere after character_id, like so: !pfactionmenu @{selected|character_id} Attacks Spell-like prepared !pfactionmenu @{selected|character_id} Attacks prepared Spell-like */ on('ready',function(){ "use strict"; const ch = function (c) { var entities = { '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '#' : '#13' }; if(_.has(entities,c) ){ return ('&'+entities[c]+';'); } return ''; }; const field = (section, id, field) => `repeating_${section}_${id}_${field}`; const repIDs = (cid, section, display) => { const repeating = findObjs({ _type: "attribute", _characterid: cid }) .filter((obj)=> { return obj.get('name').startsWith('repeating_' + section) && obj.get('name').endsWith(display); }) .map(obj => obj.get('name').replace(`repeating_${section}_`,'').replace('_' + display,'').trim()); return repeating; }; const getPrepared = (cid, section, spellid, suffix = '_spellprepared') => { const found = findObjs({ _type: "attribute", _characterid: cid }) .filter((obj)=> { return obj.get('name').startsWith(`repeating_${section}_${spellid}`) && obj.get('name').endsWith(suffix); })[0]; let prepared = 0; if(found) { prepared = found.get('current'); } return prepared; }; on('chat:message',function(msg){ if('api' === msg.type && msg.content.startsWith('!pfactionmenu') ){ let args = msg.content.split(/\s+/);; const parameters = { 'attacks': {section: 'attacks', display: 'atkname', macro: 'rollbase', title: 'Attacks'}, 'spell-like': {section: 'spell-like', display: 'spelldisplay', macro: 'rollcontent', title: 'Spell-Like Abilities'}, 'spell-0': {section: 'spell-0', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 0 Spells'}, 'spell-1': {section: 'spell-1', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 1 Spells'}, 'spell-2': {section: 'spell-2', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 2 Spells'}, 'spell-3': {section: 'spell-3', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 3 Spells'}, 'spell-4': {section: 'spell-4', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 4 Spells'}, 'spell-5': {section: 'spell-5', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 5 Spells'}, 'spell-6': {section: 'spell-6', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 6 Spells'}, 'spell-7': {section: 'spell-7', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 7 Spells'}, 'spell-8': {section: 'spell-8', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 8 Spells'}, 'spell-9': {section: 'spell-9', display: 'spelldisplay', macro: 'rollcontent', title: 'Level 9 Spells'}, } const cid = args[1] || 'N/A'; const who = getAttrByName(cid,'character_name'); if(cid === 'N/A' || who === undefined) { sendChat("Action Menu","You need to include a valid character ID."); return; } let controllers = getObj("character", cid).get('controlledby'); if(controllers !== '') controllers = controllers.split(','); // build a list of repeating sections we want to use const allowed = ['all', 'attacks', 'spells', 'spell-like', 'prepared']; let sections = []; // sections is the list of repeating sections this menu will be built from let showPrepOnly = 0; for(let i = 2; i < args.length; i++) { if(allowed.includes(args[i].toLowerCase())) { if(args[i].toLowerCase() === 'all' || args[i].toLowerCase() === 'attacks' ) { sections.push('attacks'); } if(args[i].toLowerCase() === 'all' || args[i].toLowerCase() === 'spell-like' ) { sections.push('spell-like'); } if(args[i].toLowerCase() === 'all' || args[i].toLowerCase() === 'spells' ) { sections.push('spell-0', 'spell-1', 'spell-2', 'spell-3', 'spell-4', 'spell-5', 'spell-6', 'spell-7', 'spell-8', 'spell-9'); } if(args[i].toLowerCase() === 'prepared') { showPrepOnly = 1; } } } if(sections.length === 0) { sendChat("Action Menu","Type not recognised. Enter some combination of Attacks, Spells, Spell-like, or All."); return; } log("prep: " + showPrepOnly); // now we have an array of every section on the sheet we can build the menu. let menuname = (args.length === 3 && args[2].toLowerCase() !== 'all') ? args[2].toUpperCase() : 'ACTIONS'; let icon = (sections.length ===1 && sections[0] === 'attacks') ? 'attack' : (sections.length > 1 && sections.includes('attacks')) ? 'ability' : 'spell'; let output = `&{template:pc} {{showchar=[[1]]}} {{charname=${who}}} {{type=${icon}}} {{smallname=${menuname} MENU}} {{descflag=1}} {{desc=`; sections.forEach(s => { let repeating = repIDs(cid, parameters[s].section, parameters[s].display); let founditems = []; if(repeating.length > 0) { repeating.forEach(id => { let print = true; if(showPrepOnly && s.startsWith('spell-') && !s.endsWith('-like')) { // check if spell is prepared let found = getPrepared(cid, s, id) *1||0; if(found === 0) print = false; } if(print) founditems.push(id); }); if(founditems.length > 0) { if(menuname !== 'ATTACKS' && menuname !== 'SPELL-LIKE') output += `**${parameters[s].title}**\n`; founditems.forEach(id => { output += `[@{${who}|${field(parameters[s].section,id,parameters[s].display)}}]`; output += `(!${ch('#')}${ch('@')}{${who}|${field(parameters[s].section,id,parameters[s].macro)}}) `; }) output += `\n`; } } }); output += `}}`; // time to print it out, after sorting out who needs to see it. const sender = 'character|' + cid; const caller = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); sendChat(sender,'/w ' + caller + ' ' + output); }; }); });