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

Summoncard always deals the whole deck

I added the SummonCard mod to my game and created a macro so each player draws from a different deck. But it always deals the entire deck. There is a parameter "num:x" to indicate the number of cards to deal, but no matter what I set it to, or any other parameters I try, it always deals the whole deck. Here is 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; } }); } }); }); and here is the macro: !summon-card --deck Kuro's Gun Deck --card|loc:1|num:1 !summon-card --deck Dragonlord's Gun Deck --card|loc:2|num:1 !summon-card --deck Isaiah's Gun Deck --card|loc:3|num:1 !summon-card --deck Carl's Gun Deck --card|loc:4|num:1 !summon-card --deck NPC Gun Deck --card|loc:5|num:1
1669825146
The Aaron
Roll20 Production Team
API Scripter
That script was designed for summoning all the cards that match a supplied name.  Since you are not supplying a name (or rather supplying an empty name), it matches all the cards from the deck.  I probably need to add something like |rand:1 to specify picking randomly a single card from the matches to play, which would allow you to use it the way you are intending.
1669825974

Edited 1669908578
The Aaron
Roll20 Production Team
API Scripter
I'm not set up to test this right now, but see if this works for you: !summon-card --deck Kuro's Gun Deck --card|loc:1|rand:1 !summon-card --deck Dragonlord's Gun Deck --card|loc:2|rand:1 !summon-card --deck Isaiah's Gun Deck --card|loc:3|rand:1 !summon-card --deck Carl's Gun Deck --card|loc:4|rand:1 !summon-card --deck NPC Gun Deck --card|loc:5|rand:1 (Note that num:1  is basically the default behavior, so no need to specify it) Code: on('ready',()=>{ const version = '0.1.2'; const lastUpdate = 1669825846; 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(`${_h.bold(`rand:${_h.required('number')}`)} -- Reduce the matched cards to a random subset of the specified size. If there are fewer than the specified number of cards in the matched set, all of them are used.`), _h.paragraph(`Here 2 random Wound cards are placed in a stack out of the matched Wound cards.`), _h.inset( _h.preformatted( `!summon-card {{`, ` --deck Markers`, ` --card|rand:2 Wound`, `}}` ) ), _h.paragraph(`All of these suffixes can be applied togther, though rand is usually canceled out by exact.`), _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|rand:1|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}`); } }; const pick = (ar,n) => { let s = [...ar]; let r = []; while(n-- && s.length){ let idx = randomInteger(s.length)-1; r.push(s[idx]); s.splice(idx); } return r; }; 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; } //playCardToTable(card.id,o); 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 'rand': { let cnt = parseInt(parts[1])||1; cs = pick(cs,cnt); } 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; } }); } }); });
1669831548

Edited 1669831716
Joab
Pro
I added your mod, and now I an getting a  SyntaxError: Unexpected token '.' JSLint reports Line 59, column 50: Expected '}' and instead saw '`'. Line 59 is         optional: (...o) => `${ch('[')}${o.join( ` $ { Please advise.
1669908643
The Aaron
Roll20 Production Team
API Scripter
Whoops!  I've corrected the script in the text above.  Amazing how much trouble leaving out one `.` can cause. =D   I also had the opportunity to test it and it is working the way I understand you to need it.  Please let me know if there are any other issues or requests!
Yes. It is working. Thanks.