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).

July 05 (5 years ago)

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.

July 05 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

I don't think there is one (at least not for cards), but it could be written.

July 05 (5 years ago)
keithcurtis
Forum Champion
Marketplace Creator
API Scripter

Depending on the mechanics, you might be able to do something with rollable table tokens.

July 05 (5 years ago)


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




July 05 (5 years ago)
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. 

July 05 (5 years ago)

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?

July 05 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

From the deck is trivial. There are actually some functions to do it directly for cards. 

July 05 (5 years ago)

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.

July 05 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Hahah, yeah, I can probably help with that. =D. Sorry I didn't get those waves working. 

July 05 (5 years ago)

Waves? If you're referring to a script I've asked for in the past, you have a much better memory than I do!

July 09 (5 years ago)

Would you like for me to follow up in a Private Message? Or is there anything else you'd need from me?

July 09 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Nah, I just need to have some time to poke at it. Probably this weekend. 

September 02 (5 years ago)

Edited September 02 (5 years ago)
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 DragonDragon 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 10Rogue 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.
September 02 (5 years ago)

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

September 02 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Hmmm.  What command are you using? (Or does it happen on every command with a custom deck?)

September 02 (5 years ago)

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?

September 02 (5 years ago)
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...

September 02 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Reproduction script provided to the Devs.

September 02 (5 years ago)

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!

September 02 (5 years ago)
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). 

September 02 (5 years ago)
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.

September 02 (5 years ago)

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!

September 02 (5 years ago)
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)
September 02 (5 years ago)

Edited September 03 (5 years ago)

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]]
September 02 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Ah, good point!  Yeah, I look forward to the legitimate function working. 

September 03 (5 years ago)
!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.


September 03 (5 years ago)
The Aaron
Roll20 Production Team
API Scripter

Repositions are definitely possible with the above. 

October 02 (5 years ago)

Edited October 02 (5 years ago)

I'm running into a problem pretty consistently every time I use this. Usually the pattern goes like this:

  1. I create a command to make sure I'm doing it right.
  2. I test the command (it works).
  3. I duplicate the command into a new macro, entering different card/deck names so that I can summon additional objects.
  4. The API crashes, generating the message below.
  5. 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.

October 03 (5 years ago)
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...). 

October 04 (5 years ago)

To be precise, is the distinction between images that are hosted with the "s3.amazonaws" prefix and images that are hosted elsewhere? 

October 05 (5 years ago)
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?

November 01 (5 years ago)

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:

  1. Check to see if a card with that name already exists; if so, try again or leave an error message
  2. 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?