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

Looking for a !summon script that uses default token (or cards).

I'm playing a deckbuilding game and we find that setup is taking a bit too long as we have to pull from thousands of cards across several pages to get a proper setup. We've decided that it would be faster and more convenient if there were a way to massively generate our cards by using a summoning script and having a macro call the command. What we are looking for is either a) a script that summons based on default token rather than character image (so i can set the default token to a card); or one that will let me straight up summon a two-sided card. Does any such script exist? Could anyone help me find it? Thanks.
1562303990
The Aaron
Roll20 Production Team
API Scripter
I don't think there is one (at least not for cards), but it could be written.
1562306414
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
Depending on the mechanics, you might be able to do something with rollable table tokens .
The Aaron said: I don't think there is one (at least not for cards), but it could be written. Do you know how difficult it would be? Is it something you could help with? keithcurtis said: Depending on the mechanics, you might be able to do something with rollable table tokens . Unfortunately it requires generating very specific sets of cards, and those cards need to be able to have all card functions such as take, flip, recall. I had considered that but I doubt it will work with deckbuilding mechanics
1562331583
The Aaron
Roll20 Production Team
API Scripter
Provided all the images are ones you've uploaded, and you've named all the cards, it shouldn't be hard to write a script to create one. If you want to deal them out in a particular layout, that's probably pretty doable. 
Definitely. Each card has a name already. Would it be more plausible to generate two-sided cards directly from the deck, or would I probably have to characters to make them easier to access?
1562345987
The Aaron
Roll20 Production Team
API Scripter
From the deck is trivial. There are actually some functions to do it directly for cards. 
That's really good news. As I'm not a scripter, are you able to help me? It's been years since I asked you for help on a script so I'm not sure if you'd need me to pay or what.  Thank you.
1562352769
The Aaron
Roll20 Production Team
API Scripter
Hahah, yeah, I can probably help with that. =D. Sorry I didn't get those waves working. 
Waves? If you're referring to a script I've asked for in the past, you have a much better memory than I do!
Would you like for me to follow up in a Private Message? Or is there anything else you'd need from me?
1562716155
The Aaron
Roll20 Production Team
API Scripter
Nah, I just need to have some time to poke at it. Probably this weekend. 
1567401814

Edited 1567451296
The Aaron
Roll20 Production Team
API Scripter
This got kind of big... Help is available with: !summon-card --help You can set up layouts using graphics with a particular name (doesn't need to be visible, just visible here for illustration), then summon cards to those locations: !summon-card {{ --deck core earth --card|loc:1:fit|exact core earth 1 --card|loc:2:fit core earth 2 --card|loc:3:fit core earth 3 --card|loc:4:fit core earth 4 --card|loc:5:fit core earth 5 --card|loc:6:fit core earth 6 }} Full details below: SummonCard v0.1.1 SummonCard allows dealing cards from a deck to the table easily, particularly to predefined patterns. Commands !summon-card --help Show this help. !summon-card --deck <deck name fragment> --card <card name fragment> ... This command lets you summon a card from a deck. --deck <deck name fragment>  -- Choose the deck to summon a card from.  deck name fragment  can be any subset of a deck's name, ignoring case and punctuation. --card <card name fragment>  -- Specify which card to summon.  card name fragment  follows the same rules as for decks above.  --card  must always follow a  --deck . You can specify as many  --card  arguments as you like to summon more than one card from the deck. !summon-card --deck Monsters --card beholder Note:  You can keep adding additional  --deck  and  --card  arguments to summon multiple cards from multiple decks. !summon-card --deck Monsters --card beholder --card dragon --deck Loot --card Chest Note:  You can create multi-line commands by enclosing the arguments after  !summon-card  in  {{  and  }} . !summon-card {{   --deck Monsters     --card beholder     --card dragon   --deck Loot     --card Chest }} Note:  You can use inline rolls as part of your command !summon-card --deck Monsters --card beholder --card dragon --deck Loot --card Chest [[1d3]] All cards that match a given  card name fragment  will be summoned. The following would summon all of  Dragon ,  Dragon Turtle , and  Dracolich : !summon-card {{   --deck Monsters     --card dra }} Card Options --card  can have several suffixes attached to it, separated by the  |  character. Each suffix can have zero or more arguments separated by the  :  character. There cannot be any spaces within the suffixes and arguments. loc:<label>[:fit]  -- Specifies a location where a card should be summoned. A location is a token on the same page with a name beginning with  card: . The name should have no spaces in it. Anything following  card:  is the label for that location. The graphic will be used to set the summon location and rotation. You can futher append  :fit  to the label in order to size the card to the same size as the location graphic. Here the Beholder is summoned to location 3 (a graphic with the name  card:3 ), while a chest is summoned to the location treasure (a graphic with the name  card:treasure ) and scaled to the size of the location graphic. !summon-card {{   --deck Monsters     --card|loc:3 beholder   --deck Loot     --card|loc:treasure:fit Chest }} Number based locations can be combined with inline rolls for random placement. !summon-card {{   --deck Monsters     --card|loc:[[1d6]] beholder }} show:<face | back>  -- Specifies whether the face or back of the card should be shown. Note:  Because of a bug with the API, this does not work for Marketplace cards. Here the Beholder is summoned and showing the card back, while a chest is summoned and shows the card face. !summon-card {{   --deck Monsters     --card|back beholder   --deck Loot     --card|face Chest }} exact  -- Forces a second match against the specified name, to prevent a partial match on other similar cards that contain this name as a subset. Here the  Rogue 1  card would have summoned  Rogue 10 ,  Rogue 11 , etc. as well. !summon-card {{   --deck Monsters     --card|exact Rogue 1     --card Rogue 10 }} num:<number>  -- Summon a number of duplicate cards. Here 10 Wound Markers are placed in a stack. !summon-card {{   --deck Markers     --card|num:10 Wound }} All of these suffixes can be applied togther. Here 10 Wound Markers are placed in a stack. !summon-card {{   --deck Markers     --card|loc:pool|num:10|exact|show:face Wound }} The Code: on('ready',()=>{ const version = '0.1.1'; const lastUpdate = 1567451144; const styles = { deck: `font-size: .8em;font-weight: bold;`, msg: `border: 1px solid #999; background-color: #eee; padding: .5em;` }; const s = (n)=>` style="${styles[n]||''}" `; const f = { deck: (d) => `<li ${s('deck')}>${d}</li>`, decks: (ds) => `<ul ${s('decks')}>${ds.map(f.deck).join('')}</ul>`, msg: (txt) => `<div ${s('msg')}>${txt}</div>`, key: (k) => `<code ${s('key')}>${k}</code>`, arg: (k) => `<code ${s('arg')}>${k}</code>` }; const isCleanImgsrc = (imgsrc) => /(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/.test(imgsrc); const getCleanImgsrc = (imgsrc) => { let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; const ch = (c) => { const entities = { '<' : 'lt', '>' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '*' : 'ast', '/' : 'sol', ' ' : 'nbsp' }; if( entities.hasOwnProperty(c) ){ return `&${entities[c]};`; } return ''; }; const _h = { outer: (...o) => `<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">${o.join(' ')}</div>`, title: (t,v) => `<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;">${t} v${v}</div>`, subhead: (...o) => `<b>${o.join(' ')}</b>`, minorhead: (...o) => `<u>${o.join(' ')}</u>`, optional: (...o) => `${ch('[')}${o.join(` ${ch('|')} `)}${ch(']')}`, required: (...o) => `${ch('<')}${o.join(` ${ch('|')} `)}${ch('>')}`, header: (...o) => `<div style="padding-left:10px;margin-bottom:3px;">${o.join(' ')}</div>`, section: (s,...o) => `${_h.subhead(s)}${_h.inset(...o)}`, paragraph: (...o) => `<p>${o.join(' ')}</p>`, items: (o) => `<li>${o.join('</li><li>')}</li>`, ol: (...o) => `<ol>${_h.items(o)}</ol>`, ul: (...o) => `<ul>${_h.items(o)}</ul>`, grid: (...o) => `<div style="padding: 12px 0;">${o.join('')}<div style="clear:both;"></div></div>`, cell: (o) => `<div style="width: 130px; padding: 0 3px; float: left;">${o}</div>`, inset: (...o) => `<div style="padding-left: 10px;padding-right:20px">${o.join(' ')}</div>`, pre: (...o) =>`<div style="border:1px solid #e1e1e8;border-radius:4px;padding:8.5px;margin-bottom:9px;font-size:12px;white-space:normal;word-break:normal;word-wrap:normal;background-color:#f7f7f9;font-family:monospace;overflow:auto;">${o.join(' ')}</div>`, preformatted: (...o) =>_h.pre(o.join('<br>').replace(/\s/g,ch(' '))), code: (...o) => `<code>${o.join(' ')}</code>`, attr: { bare: (o)=>`${ch('@')}${ch('{')}${o}${ch('}')}`, selected: (o)=>`${ch('@')}${ch('{')}selected${ch('|')}${o}${ch('}')}`, target: (o)=>`${ch('@')}${ch('{')}target${ch('|')}${o}${ch('}')}`, char: (o,c)=>`${ch('@')}${ch('{')}${c||'CHARACTER NAME'}${ch('|')}${o}${ch('}')}` }, bold: (...o) => `<b>${o.join(' ')}</b>`, italic: (...o) => `<i>${o.join(' ')}</i>`, font: { command: (...o)=>`<b><span style="font-family:serif;">${o.join(' ')}</span></b>` } }; const showHelp = (who) =>{ let msg = _h.outer( _h.title('SummonCard',version), _h.header( _h.paragraph('SummonCard allows dealing cards from a deck to the table easily, particularly to predefined patterns.') ), _h.subhead('Commands'), _h.inset( _h.font.command( `!summon-card`, `--help` ), _h.paragraph('Show this help.') ), _h.inset( _h.font.command( `!summon-card`, `--deck ${_h.required('deck name fragment')}`, `--card ${_h.required('card name fragment')}`, `...` ), _h.paragraph('This command lets you summon a card from a deck.'), _h.ul( `${_h.bold(`--deck ${_h.required('deck name fragment')}`)} -- Choose the deck to summon a card from. ${_h.bold('deck name fragment')} can be any subset of a deck's name, ignoring case and punctuation.`, `${_h.bold(`--card ${_h.required('card name fragment')}`)} -- Specify which card to summon. ${_h.bold('card name fragment')} follows the same rules as for decks above. ${_h.bold('--card')} must always follow a ${_h.bold('--deck')}. You can specify as many ${_h.bold('--card')} arguments as you like to summon more than one card from the deck.` ), _h.inset( _h.preformatted( `!summon-card --deck Monsters --card beholder` ) ), _h.paragraph(`${_h.bold('Note:')} You can keep adding additional ${_h.bold('--deck')} and ${_h.bold('--card')} arguments to summon multiple cards from multiple decks.`), _h.inset( _h.preformatted( `!summon-card --deck Monsters --card beholder --card dragon --deck Loot --card Chest` ) ), _h.paragraph(`${_h.bold('Note:')} You can create multi-line commands by enclosing the arguments after ${_h.code('!summon-card')} in ${_h.code('{{')} and ${_h.code('}}')}.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card beholder`, ` --card dragon`, ` --deck Loot`, ` --card Chest`, `}}` ) ), _h.paragraph(`${_h.bold('Note:')} You can use inline rolls as part of your command`), _h.inset( _h.preformatted( `!summon-card --deck Monsters --card beholder --card dragon --deck Loot --card Chest ${ch('[')}${ch('[')}1d3${ch(']')}${ch(']')}` ) ), _h.paragraph(`All cards that match a given ${_h.bold('card name fragment')} will be summoned. The following would summon all of ${_h.bold('Dragon')}, ${_h.bold('Dragon Turtle')}, and ${_h.bold('Dracolich')}:`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card dra`, `}}` ) ), _h.section('Card Options', _h.paragraph(`${_h.bold('--card')} can have several suffixes attached to it, separated by the ${_h.code('|')} character. Each suffix can have zero or more arguments separated by the ${_h.code(':')} character. There cannot be any spaces within the suffixes and arguments.`), _h.paragraph(`${_h.bold(`loc:${_h.required('label')}${_h.optional(':fit')}`)} -- Specifies a location where a card should be summoned. A location is a token on the same page with a name beginning with ${_h.code('card:')}. The name should have no spaces in it. Anything following ${_h.code('card:')} is the label for that location. The graphic will be used to set the summon location and rotation. You can futher append ${_h.bold(':fit')} to the label in order to size the card to the same size as the location graphic.`), _h.paragraph(`Here the Beholder is summoned to location 3 (a graphic with the name ${_h.code('card:3')}), while a chest is summoned to the location treasure (a graphic with the name ${_h.code('card:treasure')}) and scaled to the size of the location graphic.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card|loc:3 beholder`, ` --deck Loot`, ` --card|loc:treasure:fit Chest`, `}}` ) ), _h.paragraph(`Number based locations can be combined with inline rolls for random placement.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card|loc:${ch('[')}${ch('[')}1d6${ch(']')}${ch(']')} beholder`, `}}` ) ), _h.paragraph(`${_h.bold(`show:${_h.required('face','back')}`)} -- Specifies whether the face or back of the card should be shown.`), _h.paragraph(`${_h.bold('Note:')} Because of a bug with the API, this does not work for Marketplace cards.`), _h.paragraph(`Here the Beholder is summoned and showing the card back, while a chest is summoned and shows the card face.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card|back beholder`, ` --deck Loot`, ` --card|face Chest`, `}}` ) ), _h.paragraph(`${_h.bold(`exact`)} -- Forces a second match against the specified name, to prevent a partial match on other similar cards that contain this name as a subset.`), _h.paragraph(`Here the ${_h.code('Rogue 1')} card would have summoned ${_h.code('Rogue 10')}, ${_h.code('Rogue 11')}, etc. as well.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Monsters`, ` --card|exact Rogue 1`, ` --card Rogue 10`, `}}` ) ), _h.paragraph(`${_h.bold(`num:${_h.required('number')}`)} -- Summon a number of duplicate cards.`), _h.paragraph(`Here 10 Wound Markers are placed in a stack.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Markers`, ` --card|num:10 Wound`, `}}` ) ), _h.paragraph(`All of these suffixes can be applied togther.`), _h.paragraph(`Here 10 Wound Markers are placed in a stack.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Markers`, ` --card|loc:pool|num:10|exact|show:face Wound`, `}}` ) ) ) ) ); sendChat('',`/w "${who}" ${msg}`); }; const keyFormat = (s)=>s.toLowerCase().replace(/[^a-z0-9]/g,''); const lookupDecks = (()=>{ let decks = findObjs({ type: 'deck' }).reduce( (m,d) => (m[d.id]=d) && m, {}); let lookup = Object.keys(decks).reduce( (m,k) => (m[keyFormat(decks[k].get('name'))]=k) && m, {}); on('add:deck',(d)=>{ decks[d.id]=d; lookup[keyFormat(d.get('name'))]=d.id; }); on('change:deck',(d,p)=>{ if(d.get('name') !== p.name){ delete lookup[keyFormat(p.name)]; lookup[keyFormat(d.get('name'))]=d.id; } }); on('destroy:deck',(d)=>{ delete decks[d.id]; delete lookup[keyFormat(d.get('name'))]; }); return (nameFragment) => { let key = keyFormat(nameFragment); return Object.keys(lookup).filter( k => -1 !== k.indexOf(key)).map(k => decks[lookup[k]]); }; })(); const lookupCards = (()=>{ let cards = findObjs({ type: 'card' }).reduce( (m,c) => { let did = c.get('deckid'); m[did] = m[did]||{}; m[did][c.id]=c; return m; },{}); let lookup = Object.keys(cards).reduce( (memo, did) => { memo[did]=Object.keys(cards[did]).reduce( (m,k) => (m[keyFormat(cards[did][k].get('name'))]=k) && m, {}); return memo; },{}); on('add:card',(c)=>{ if('card'===c.get('type')){ let did = c.get('deckid'); cards[did] = cards[did]||{}; cards[did][c.id]=c; lookup[did]=lookup[did]||{}; lookup[did][keyFormat(c.get('name'))]=c.id; } }); on('change:card',(c,p)=>{ if('card'===c.get('type') && c.get('name') !== c.name){ let did = c.get('deckid'); delete lookup[did][keyFormat(p.name)]; lookup[did][keyFormat(c.get('name'))]=c.id; } }); on('destroy:card',(c)=>{ if('card'===c.get('type')){ let did = c.get('deckid'); delete cards[did][c.id]; delete lookup[did][keyFormat(c.get('name'))]; } }); return (deckid, nameFragment) => { let key = keyFormat(nameFragment); return Object.keys(lookup[deckid]).filter( k => -1 !== k.indexOf(key)).map(k => cards[deckid][lookup[deckid][k]]); }; })(); const getPageForPlayer = (playerid) => { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; const range = (n) => [...Array(n).keys()]; const sendError = (who, msg) => sendChat('',`/w "${who}" ${f.msg(msg)}`); const getLocations = (pageid) => findObjs({type:'graphic', pageid}) .filter(g=>/^card:/i.test(g.get('name'))) .reduce( (m,l) => (m[l.get('name').replace(/^card:/i,'')]=l) && m, {}); const processInlinerolls = (msg) => { if(_.has(msg,'inlinerolls')){ return _.chain(msg.inlinerolls) .reduce(function(m,v,k){ let ti=_.reduce(v.results.rolls,function(m2,v2){ if(_.has(v2,'table')){ m2.push(_.reduce(v2.results,function(m3,v3){ m3.push(v3.tableItem.name); return m3; },[]).join(', ')); } return m2; },[]).join(', '); m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } else { return msg.content; } }; const fixedPlayCardToTable = (cardid, options) => { let card = getObj('card',cardid); if(card){ let deck = getObj('deck',card.get('deckid')); if(deck){ if(!isCleanImgsrc(deck.get('avatar')) && !isCleanImgsrc(card.get('avatar'))){ // marketplace-marketplace: playCardToTable(cardid, options); } else if (isCleanImgsrc(deck.get('avatar')) && isCleanImgsrc(card.get('avatar'))){ let pageid = options.pageid || Campaign().get('playerpageid'); let page = getObj('page',pageid); if(page){ let imgs=[getCleanImgsrc(card.get('avatar')),getCleanImgsrc(deck.get('avatar'))]; let currentSide = options.hasOwnProperty('currentSide') ? options.currentSide : ('faceup' === deck.get('cardsplayed') ? 0 : 1 ); let width = options.width || parseInt(deck.get('defaultwidth')) || 140; let height = options.height || parseInt(deck.get('defaultheight')) || 210; let left = options.left || (parseInt(page.get('width'))*70)/2; let top = options.top || (parseInt(page.get('height'))*70)/2; createObj( 'graphic', { subtype: 'card', cardid: card.id, pageid: page.id, currentSide: currentSide, imgsrc: imgs[currentSide], sides: imgs.map(i => encodeURIComponent(i)).join('|'), left,top,width,height, layer: 'objects', isdrawing: true, controlledby: 'all', gmnotes: `cardid:${card.id}` }); } else { sendError('gm',`Specified pageid does not exists.`); } } else { sendError('gm',`Can't create cards for a deck mixing Marketplace and User Library images.`); } } else { sendError('gm',`Cannot find deck for card ${card.get('name')}`); } } else { sendError('gm',`Cannot find card for id ${cardid}`); } }; on('chat:message', msg => { if( 'api' === msg.type && /^!summon-card\b/i.test(msg.content)){ let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let pageid = getPageForPlayer(msg.playerid); let args = processInlinerolls(msg) .replace(/<br\/>\n/g, ' ') .replace(/(\{\{(.*?)\}\})/g," $2 ") .split(/\s+--/); if(args.find(n=>/^help(\b|$)/i.test(n))){ showHelp(who); return; } let locs = getLocations(pageid); let currentDeck; args.forEach( a => { let params = a.trim().split(/\s+/); let cmd = params[0].split(/\|/); switch(cmd[0].toLowerCase()){ case 'deck': { let dn = params.slice(1).join(' '); let ds = lookupDecks(dn); if(1 === ds.length){ currentDeck = ds[0]; } else if(ds.length>1){ let min = ds.filter(d=>d.get('name').trim().toLowerCase()===dn.trim().toLowerCase()); if(1===min.length){ currentDeck = min[0]; } else { sendError(who,`Too many decks matching ${f.key(dn)}: ${f.decks(ds.map(d=>d.get('name')))}`); currentDeck = undefined; } } else { sendError(who,`No decks matching ${f.key(dn)}`); currentDeck = undefined; } } break; case 'card': { let cn = params.slice(1).join(' '); if(currentDeck){ let cs = lookupCards(currentDeck.id,cn); if(cs.length){ let opts = () => ({pageid}); let mutator = () => {}; let place = (card) => { let o = opts(); if(o.hasOwnProperty('currentSide') && ! isCleanImgsrc(card.get('avatar'))) { sendError(who,`Can't adjust side on Marketplace Decks currently. Change the <b>Played Facing</b> setting for the deck.`); delete o.currentSide; } fixedPlayCardToTable(card.id,o); setTimeout(()=>{ //findObjs(Object.assign({},o,{type:'graphic',subtype:'card',cardid:card.id})) findObjs(Object.assign({},o,{type:'graphic'})) .filter(g => ('card'===g.get('subtype') && card.id === g.get('cardid')) || (g.get('gmnotes').includes(`cardid:${card.id}`))) .forEach(g=>mutator(g)); },100); }; cmd.slice(1).forEach(c=>{ let parts = c.split(/:/); switch(parts[0]){ case 'num': { place = (()=>{ let cnt = parseInt(parts[1])||1; let oldPlace = place; return (card) => range(cnt).forEach(()=>oldPlace(card)); })(); } break; case 'exact': { place = (()=>{ let oldPlace = place; return (card) => { if(card.get('name').trim().toLowerCase() === cn.trim().toLowerCase()){ oldPlace(card); } }; })(); } break; case 'loc': if(locs.hasOwnProperty(parts[1])){ let sizeOpts = {}; if('fit'===parts[2]){ let loc = locs[parts[1]]; sizeOpts = { width: loc.get('width'), height: loc.get('height') }; } opts = (()=>{ let loc = locs[parts[1]]; let oldOpts = opts; return (o) =>Object.assign( {}, sizeOpts, { left: loc.get('left'), top: loc.get('top') }, (oldOpts(o) || {}) ); })(); mutator = (()=>{ let loc = locs[parts[1]]; let oldMutator = mutator; return (g) => { oldMutator(g); g.set({ rotation: loc.get('rotation') }); }; })(); } break; case 'show': if(['face','back'].includes(parts[1].toLowerCase())){ opts = (()=>{ let side=('face'===parts[1].toLowerCase() ? 0 : 1); let oldOpts = opts; return (o) =>{ let oo = oldOpts(o) || {}; oo.currentSide = side; return oo; }; })(); } break; } }); cs.forEach(c=>place(c)); } } else { sendError(who,`No deck set to find card ${f.key(cn)}. Use ${f.arg('--deck [name]')} to specify a deck first.`); } } break; } }); } }); }); Update v0.1.1 -- Wrote my own playCardToTable() which can create pseudo cards for User Library image decks.  It can't create card objects, merely multisided tokens that behave like cards.  There are some differences, but it should work for most use cases until the real playCardToTable() function is fixed.
Awesome script, extremely useful! But I keep running into this same error as I have for a while. I believe I may be doing something - I get this error when using a custom deck (images that I uploaded myself, not marketplace). I also only have TokenMod and this script installed on a game I am trying it on. When using the default "Playing Cards" deck everything works fine though. TypeError: Cannot read property 'set' of undefined TypeError: Cannot read property 'set' of undefined at playCardToTable (/home/node/d20-api-server/api.js:2478:8) at place (apiscript.js:3052:41) at cs.forEach.c (apiscript.js:3141:51) at Array.forEach (native) at args.forEach.a (apiscript.js:3141:40) at Array.forEach (native) at msg (apiscript.js:3015:18) at eval (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:65:16) at Object.publish (eval at <anonymous> (/home/node/d20-api-server/api.js:151:1), <anonymous>:70:8) at /home/node/d20-api-server/api.js:1634:12
1567429547
The Aaron
Roll20 Production Team
API Scripter
Hmmm.  What command are you using? (Or does it happen on every command with a custom deck?)
I was trying that basic one, changing the name to my deck's name and card. If I do not specify a card, simply nothing happens. If I do specify a card, it produces the error messages. !summon-card --deck Tester !summon-card --deck Tester --card Duelist Though if this is working for you, then it probably is something on my end, right? Perhaps my image files or that's not it?
1567432686
The Aaron
Roll20 Production Team
API Scripter
Ok, I've confirmed that it does the same thing for me with custom card decks!  BOO!  Writing a reproduction script now to drop to the devs...
1567445194
The Aaron
Roll20 Production Team
API Scripter
Reproduction script provided to the Devs.
Wow, this is already so much cooler than I imagined!  I haven't had time to test it out yet, but thank you so much! Out of curiosity, can this summon several cards from several decks at once? If I can I'd like to summon about a dozen from the same command. This looks like it goes above and beyond!
1567449879
The Aaron
Roll20 Production Team
API Scripter
Yup, it can summon many cards from many decks on a single command. I'm working on a manual playCardToTable() that won't crash the API for User Library images (grumble, grumble). 
1567451381
The Aaron
Roll20 Production Team
API Scripter
Ok, I updated the above post with a new version that can play cards to the table for User Library cards.  CAVEAT: They are not played as cards, but as multi-sided tokens.  They should behave mostly like played cards, but won't have a Flip Card option, and can't be picked up or recalled to the deck.  However, they should work for most of the use cases discussed above.
A few questions: I might be able to solve this with a macro, but is it possible (perhaps via card options) to have the option to prevent cards from summoning in occupied locations? This would allow users to partially refill designated summon locations (I can see this being useful in games like Illimat and a few Deckbuilding games, to name a few) What are "User Library Cards?" I'm not familiar with the term. How would I use them? Does this follow any rules of randomness applied by the deck in roll20 (mainly is it exhaustible in any sense)? Thank you again. This rocks!
1567456260
The Aaron
Roll20 Production Team
API Scripter
A few answers: It would be possible to have a selective loading of locations.  Should break this down a bit to be sure all the cases are covered. By User Library cards/images, I mean decks with cards that have images you uploaded, as opposed to images taken from the Marketplace directly.  Probably the primary deck case for most people. Right now, it only draws an explicit card, so zero randomness.  I was planning on adding random cards next. Without being able to use the playCardToTable() function for User Library cards/images, the "cards" are not tied to the deck, and can't be picked up like cards, or flipped like cards, which makes me sad.  I've been talking to one of the Devs about it, so I hope they can sort it out soon and I can revert it to just playing the cards with their function. Robert R.  said: Unfortunately it requires generating very specific sets of cards, and those cards need to be able to have all card functions such as take, flip, recall. I had considered that but I doubt it will work with deckbuilding mechanics Based on this, version 0.1.1 probably won't work for your use case yet, but we can use it to prototype what will work once the Roll20 playCardToTable() function works correctly.  Nicholas seemed pretty intent on sorting these issues out, so hopefully that will be the case. So, just thinking off the cuff, some things I probably should add: Random card(s) from a deck Random card(s) from several decks Refill stack to some number Dealing to a layout, a random set of cards, filling each position Dealing only to open spaces Randomize up or down orientation on deal (good for tarot type things)
1567458481

Edited 1567531167
Nice rework, pretty useful as is! Will be super nice with real cards eventually, can't wait! For my own game, I did find a way to get random cards from a deck with your current script. Figured I'd share in case someone else finds it useful too. I used numbers for the card names in the deck and use the following for a random card: !summon-card --deck Glass --card [[1d5]]
1567460138
The Aaron
Roll20 Production Team
API Scripter
Ah, good point!  Yeah, I look forward to the legitimate function working. 
!summon-card --deck Glass --card [[1d5]] Yeah this is what I had in mind. I was wondering if repetitions are theoretically possible within the same command. Haven't had time to test it.
1567510506
The Aaron
Roll20 Production Team
API Scripter
Repositions are definitely possible with the above. 
1570002360

Edited 1570002674
I'm running into a problem pretty consistently every time I use this. Usually the pattern goes like this: I create a command to make sure I'm doing it right. I test the command (it works). I duplicate the command into a new macro, entering different card/deck names so that I can summon additional objects. The API crashes, generating the message below. The original macro I made still works just fine. Is this essentially the same issue that Inza mentioned? For all intents and purposes, the commands I'm using are identical. I've also checked dozens of times to make sure there's no typo or erroneous information in the macro. It literally just seems to work for some card names and not for others. Edit: Something very interesting I noticed when trying to duplicate this: If I enter the specifications for a card that works and a card that doesn't work in the same command, in that order, the script partially completes, creating an object for the former. What's weird is that the object it creates is not a card at all; it is in fact a multi-sided object (ie rollable table) whose sides correspond to the front and back of the card in question. It can't be picked up or flipped, but it can be rolled on both sides. Not sure if that's particularly necessary feedback, but I thought it might help you to identify what's happening.
1570069500
The Aaron
Roll20 Production Team
API Scripter
If I remember digging into this, the functions for creating cards by playing them to the table work for marketplace decks, but not custom decks (which is an odd reversal...). 
To be precise, is the distinction between images that are hosted with the "s3.amazonaws" prefix and images that are hosted elsewhere? 
1570240918
The Aaron
Roll20 Production Team
API Scripter
The ones with marketplace in the URL seem to work. 
Is there a Summon Script out there that uses Default Tokens?
I'd like to utilize this script in a way that doesn't allow replacements. Is this possible? Two logical routes I'm considering but I don't know if they're plausible with the current API: Check to see if a card with that name already exists; if so, try again or leave an error message Totally delete the card from the deck file on roll20. Assuming I run the command with a duplicate deck this allows me to run the deck empty every game. Thoughts?