
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