Shortcuts for working with virtual cards.
This shortcut-file has a tutorial video available: Using the "cards" shortcut-file to use virtual cards (runtime 12:43)
Here are some good card images you can download to try this out: Playing cards - Standard playing cards. Nice quality, though rather large file-size. Tarot cards - Good quality tarot cards under public domain. I used these in the video tutorial.
Thanks to luciellaes on itch.io for cleaning up and providing the tarot images.
This shortcut-file has supplementary shortcut files: cards_pileviewer.sfile - Adds an Obsidian panel to view card-decks. cards_ui.sfile - Graphical UI versions of all of these shortcuts.
__ __
// Trigger the event callback calls for a change in what piles are available
function onPileListChanged()
{
_inlineScripts.inlineScripts.HelperFncs.
callEventListenerCollection(
"cards.onPileListChanged",
_inlineScripts.cards.listeners.onPileListChanged);
}
// Trigger the event callback calls for a change in a pile
function onPileChanged(pileName)
{
_inlineScripts.cards.listeners.changedPile = pileName;
_inlineScripts.inlineScripts.HelperFncs.
callEventListenerCollection(
"cards.onPileChanged",
_inlineScripts.cards.listeners.onPileChanged);
}
// Get the current back-image, url
function getBackImage()
{
return _inlineScripts.state.sessionState.cards.backImage ||
_inlineScripts.cards.defaultBackImage;
}
// Turn a relative path url into an absolute path url based in the vault's root
function getAbsolutePath(path)
{
if (path.startsWith("data:image")) { return path; }
path = app.vault.fileMap[path];
if (!path) { return ""; }
return app.vault.getResourcePath(path);
}
// Create a block of html to represent a specific card
function createCardUi(isFaceUp, card, id, scale, includeDataSrc)
{
const result = document.createElement("img");
result.classList.add("cardUi");
result.src = getAbsolutePath(isFaceUp ? card.path : getBackImage());
const size = _inlineScripts.state.sessionState.cards.size * (scale || 1.0);
result.style.width = size + "px";
result.style.height = (size * card.aspect) + "px";
result.dataset.id = id;
if (includeDataSrc)
{
result.dataset.src = getAbsolutePath(card.path);
}
if (isFaceUp)
{
switch (card.rotation)
{
case 1: result.classList.add("rotated1"); break;
case 2: result.classList.add("rotated2"); break;
case 3: result.classList.add("rotated3"); break;
}
}
return result;
}
// Create a block of markdown to represent a specific card
function createCardUi_inNote(isFaceUp, card)
{
// Figure out what image to use
let path = isFaceUp ? card.path : getBackImage();
// Generate and return the text for the markdown card ui
const alt = (!card.rotation || !isFaceUp) ? "" : "rotated" + card.rotation;
const width = _inlineScripts.state.sessionState.cards.size;
const height = Math.trunc(width * card.aspect);
return "![" + alt + "|" + width + "x" + height + "](<" + path + ">) ";
}
__ Helper scripts
__
^sfile setup$
__
const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath;
// Confirm state exists. If not setup default state.
confirmObjectPath(
"_inlineScripts.state.sessionState.cards",
{ piles: {}, size: 150, priorShowMoved: true, backImage: null });
// Event callbacks for state system events
confirmObjectPath(
"_inlineScripts.state.listeners.onReset.cards",
function()
{
expand("cards reset noconfirm");
});
confirmObjectPath(
"_inlineScripts.state.listeners.onLoad.cards",
function()
{
onPileListChanged();
onPileChanged(null);
});
// Confirm that cards system events are ready
confirmObjectPath("_inlineScripts.cards.listeners.onPileListChanged");
confirmObjectPath("_inlineScripts.cards.listeners.onPileChanged");
// Custom CSS
_inlineScripts.inlineScripts.HelperFncs.addCss("cards", ".rotated1, img[alt='rotated1'] { transform: rotate(90deg); } .rotated2, img[alt='rotated2'] { transform: rotate(180deg); } .rotated3, img[alt='rotated3'] { transform: rotate(270deg); } .rotated, img[alt='rotated'] { transform: rotate(180deg); } .iscript_cardChoice { filter: brightness(75%); cursor: pointer; border-width: 4px; border-style: solid;border-color: black; } .iscript_cardChoice:hover { filter:brightness(100%); } .iscript_cardSelected { filter:brightness(100%); border-color: yellow; }");
// Default card back image (hardcoded as base64)
confirmObjectPath("_inlineScripts.cards.defaultBackImage",
`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABkCAYAAAAlg3YKAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAEvElEQVR42u3dP2wTZxjH8e9diD2UYFdWKwuQXGpTEjxYSFkSIcuDJYTqZrmweSrZzixlZ2BPh+J3M52ytMILtVQhPFiQhCUS8uCiCKzUA8hSdaoP6OBE5Dr4znFCYiiY5o+f33TRvXlP/uTx+zqPLL0aPWk2nS9HR7kGZICzwGcMR/4BngIln4+fxsa0v7wbmndhWc4s8DMwxnDnJfB9KKQVu0CW5VwBfgG01dVNlpY2ef58k42N4RAZHYXTp3WmpnTGx3UAB7gSCmlFzX1bPQPG7t9/w8OHb4a6fJLJEdLpEYCXPh8x3V1zxlZXN4ceB+DBgzesrm4CnFhf55oOzAAsLW0i6WR5uWvxnQ58DfDihQB56bGI6cBxgPV1gfHSbncvj+vC0T8CJEACJEACJEACJEACJBEgARIgARIgARIgAZIIkAAJkAAJkAAJkABJBOjAAtnVFEr5USpF1d5lQMNEVe7tO9CxwU11j4qaodb9eQ7TVHuODiQqmIk61eLV3QdEFGaEowR0iZTZ5kI1RZnbGInokXiLHfvkT2iYqDWIWwVqzSnimdukIv3w6lSL51lsAvG7mKlL75zHbpiUSwWaAOE5MoYicqjWIGuCC0YbMzuLtfI7dt/BURJGGzMz937z2HnKKxNMZtuYZptsDFaq9UNUQQChswQAAmcJ8XSw87Se0GwWKC1c3xoXnwGihwioT7UEQ49Ya0HnlX9AghOEw/OkjdwHT7EPu9h51GL/Xay7WV2YZ2XBj2IK06yAnae4cL2znlBA1SB88Q+M4F7bYY70pElZ+d3fcccPaJPQLMtxAG7ckC8p9ubmTZ98kpZ/NQRIgARIgARIgARIIkACNBxAB6QPPVCgRjVFUflRyk+xkqdhf8RkEbXVQXxn6lSLJo1PDHTsY//ipWezZM0KAcC28/zZqhMJRI/MW2wg/aCWXYdAlEAgR8LtWm3rEzNHxnT7xNt6y7h959h79KEhHJ8nncoRaJioUqEzRhXA7SVVi/7OHOyYZ9+AIooseR6Xr2I1H9EMz5M1cgTsPOUSTGbbRHZr89XgTLZNqudewmiTcEG2xYIz6c5Yu5qiXL2MkVCY5g9Uiz8S7GnQL4bc5x+kCgpEcqQiuU7VeC8g+IRmfGZ3HIB+93YmtDU2EIzTXHvGXv3mDHcoF+8AcWJp1a3m/VukG3kqjXp30Wz97V4GJwjX7n7cgt2ttq157FaN8OexbaXY6nlGJFXBMCoYk7D4+N4BqKDIZc5UrqJKjwB3jUhEgRzpjEl5wU9p5xq0W/r1oeOwVvZT6q5BXvVE+SoGCwt+Ft01SCm/e2+Ki1k1EKCD3ZNumKi1mYEstv810pP+P7f5T5YD8AUGqSABEiABEiABEiABEiCJAAmQAAmQAAmQAAmQRIAESIAESIAESIAESCJAAjQAoNcAPp9gePH7u5cvdaAOcPKkFJMXz8JxqOvAbwDT0wLkxbPQNEr6xga3gFfj4zrJ5MjQ4ySTI5w7p0Pn8KNb3vFZs8CvuMdnLS93js8altNafD44dUpnelr3cLaOz/IGWZZj0DmA7cSQF9HbB7B5efXK+WJ9nWuaxreOwze45/4MQV47Dk81jZLPx63eI/z+BcWKtwx4PjyWAAAAAElFTkSuQmCC`);
// Custom popop definition for picking cards
confirmObjectPath("_inlineScripts.cards.cardPickerPopup",
{
// Setup function
onOpen: async (data, parent, firstButton, settingType) =>
{
// Parent ui element
data.ui = parent;
parent.style["overflow-y"] = "scroll";
parent.parentNode.style.display = "flex";
parent.parentNode.style["flex-direction"] = "column";
parent.nextElementSibling.style["margin-top"] = ".5em"
// Card visualization elements
const pile = _inlineScripts.state.sessionState.cards.piles[data.pileId];
for (let i = pile.cards.length - 1; i >= 0; i--)
{
const img = createCardUi(true, pile.cards[i], i, 1.0, true)
parent.append(img);
img.classList.add("iscript_cardChoice");
img.addEventListener("pointerdown", function()
{
this.classList.toggle("iscript_cardSelected");
});
}
},
// Shutown function
onClose: async (data, resolveFnc, buttonId) =>
{
if (buttonId !== "Ok") { return; }
// Create a list of the selected cards
let result = [];
const children = data.ui.children;
for (var i = 0; i < children.length; i++)
{
if (children[i].classList.contains("iscript_cardSelected"))
{
result.push(children[i].dataset.id)
}
}
// Return the list of selected cards
resolveFnc(result);
}
});
__ Sets up this shortcut-file
__
^sfile shutdown$
__
// Event callback removal
delete _inlineScripts.state?.listeners?.onReset?.cards;
delete _inlineScripts.state?.listeners?.onLoad?.cards;
// Custom CSS removal
_inlineScripts.inlineScripts.HelperFncs.removeCss("cards");
// State removal
delete _inlineScripts.state?.sessionState?.cards;
// Session state removal
delete _inlineScripts.cards;
__ Shuts down this shortcut-file
__
^cards? reset$
__
// Confirm
if (!popups.confirm("Confirm resetting the <b>Cards</b> system")) { return null; }
// Reset
expand("cards reset noconfirm");
return expFormat("All card-piles cleared.");
__ cards reset - Clears all card-piles.
__
^cards? reset noconfirm$
__
const confirmObjectPath = _inlineScripts.inlineScripts.HelperFncs.confirmObjectPath;
// Make sure state container is ready
confirmObjectPath("_inlineScripts.state.sessionState");
// Recreate default state
_inlineScripts.state.sessionState.cards =
{ piles: {}, size: 150, priorShowMoved: true, backImage: null };
// Trigger the event of the list of piles changing
onPileListChanged();
__ hidden - No-confirm reset
__
^cards? settings ?([1-9][0-9]*|) ?("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+|default|)$
__
// Remove quotes around the path
$2 = $2.replace(/^"(.*)"$/, "$1")
// Start expansion result
let result = "The card system's settings are updated:\n";
// Size
let sizeChanged = false;
if ($1 && $1 != _inlineScripts.state.sessionState.cards.size)
{
_inlineScripts.state.sessionState.cards.size = $1;
result += ". __Card size__ is changed to __" + $1 + "__ _(pixels)_.\n";
sizeChanged = true;
}
else
{
$1 = _inlineScripts.state.sessionState.cards.size;
result += ". __Card size__ remains __" + $1 + "__ _(pixels)_.\n";
}
// Back image
let backImageFailedToChange = false;
let backImageChanged = false;
if ($2)
{
if ($2.toLowerCase() === "default")
{
if (_inlineScripts.state.sessionState.cards.backImage)
{
_inlineScripts.state.sessionState.cards.backImage = null;
result += ". __Back-image__ reset to default.";
backImageChanged = true;
}
}
else if ($1 !== _inlineScripts.state.sessionState.cards.backImage)
{
const file = app.vault.fileMap[$2];
if (!file)
{
result +=
". __Back-image__ not changed. File __" + $2 + "__ was not found.";
backImageFailedToChange = true;
}
else if (file.children)
{
result += ". __Back-image__ not changed. __" + $2 + "__ is not a file.";
backImageFailedToChange = true;
}
else
{
_inlineScripts.state.sessionState.cards.backImage = $2;
result += ". __Back-image__ changed to __" + $2 + "__.";
backImageChanged = true;
}
}
}
if (!backImageChanged && !backImageFailedToChange)
{
$2 = _inlineScripts.state.sessionState.cards.backImage;
$2 = $2 ? ("__" + $2 + "__") : "at default";
result += ". __Back-image__ remains " + $2 + ".";
}
// React to changes
if (sizeChanged || backImageChanged) { onPileChanged(); }
return expFormat(result);
__ cards settings {size: >0, default: ""} {back-image: path text, default: ""} - Updates the settings for the cards system. - {size} - The width for all cards, in pixels. Card height scales to match. - {back-image} - The path to an image file to represent face-down cards, or "default" to reset to the default back-image.
__
^cards? pile ([_a-zA-Z][_a-zA-Z0-9]*) ?(up|down|) ?(y|n|)$
__
// Early out if name is already used: prevent overwriting existing pile with new one
if (_inlineScripts.state.sessionState.cards.piles[$1])
{
return expFormat(
"Card-Pile not created. " + "The __" + $1 + "__ card-pile already exists.");
}
// If "show moved" parameter is passed, remember it.
if ($3)
{
_inlineScripts.state.sessionState.cards.priorShowMoved = ($3 === "y");
}
// Create the new pile
const showMoved = _inlineScripts.state.sessionState.cards.priorShowMoved;
_inlineScripts.state.sessionState.cards.piles[$1] =
{ cards: [], isFaceUp: ($2 === "up"), showMoved };
// Trigger the event of the list of piles changing
onPileListChanged();
return expFormat("The __" + $1 + "__ card-pile is created.");
__ cards pile {pile id: name text} {facing: up OR down, default: down} {show moved: y OR n, default: prior} - Creates an empty card-pile {pile id} with all cards facing {facing} (face-up or face-down). If {show moved}, cards that are drawn or picked into the {pile id} card-pile are also printed to the note.
__
^cards? remove ([_a-zA-Z][_a-zA-Z0-9]*) ?(y|n|)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Card-pile not removed. The __" + $1 + "__ card-pile was not found.");
}
// Confirm with user
if (!popups.confirm("Confirm destroying the <b>" + $1 + "</b> card-pile."))
{
return null;
}
// Start expansion result
let result = "";
// Recall all cards (if parameter says to do so)
if ($2 === "y")
{
const cardCount = pile.cards.length;
let recallCount = 0;
for (let i = cardCount - 1; i >= 0; i--)
{
const card = pile.cards[i];
// Don't recall a card that originates from THIS pile
if (card.origin === $1) { continue; }
// Get the origin pile
const originPile =
_inlineScripts.state.sessionState.cards.piles[card.origin];
if (!originPile) { continue; }
// Move the card to the origin pile
originPile.cards.push(card);
pile.cards.splice(i, 1);
// Track how many cards are recalled
recallCount++;
}
// Add a notification of this recall to the expansion
result +=
"__" + recallCount + " / " + cardCount + "__ cards in the __" + $1 +
"__ card-pile are recalled to other card-piles.\n";
// Notify of all piles (potentially) changing
if (recallCount > 0)
{
onPileChanged(null);
}
}
// Add notification about removing this pile
const cardRemoveCount =
_inlineScripts.state.sessionState.cards.piles[$1].cards.length;
result += "The __" +
$1 + "__ card-pile is removed along with __" + cardRemoveCount +
"__ cards.";
// Remove the pile
delete _inlineScripts.state.sessionState.cards.piles[$1];
// Re-origin any orphaned cards - cards that have the removed pile as their origin.
let reOriginCount = 0;
for (const key in _inlineScripts.state.sessionState.cards.piles)
{
const pileToSearch = _inlineScripts.state.sessionState.cards.piles[key];
for (const card of pileToSearch.cards)
{
if (card.origin === $1)
{
card.origin = key;
reOriginCount++;
}
}
}
// Add notification of re-origining orphaned cards.
if (reOriginCount)
{
result += "\n__" + reoriginCount + "__ orphaned cards were re-origined.";
}
// Trigger the event of the list of piles changing
onPileListChanged();
return expFormat(result);
__ cards remove {pile id: name text} {recall: y OR n, default: n} - Removes the {pile id} card-pile, including all cards within it. If {recall}, all cards in {pile id} are recalled before the {pile id} card-pile is removed. If the {pile id} card-pile is the origin for any cards, then they are orphaned, and immediately re-origined to their current card-pile.
__
^cards? pilesettings ([_a-zA-Z][_a-zA-Z0-9]*) ?(up|down|) ?(y|n|)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Card-Pile not changed. The __" + $1 + "__ card-pile was not found.");
}
// Start expansion notification
let result = "The __" + $1 + "__ card-pile's settings are updated:\n";
// Update facing
if ($2 && pile.isFaceUp !== ($2 === "up"))
{
pile.isFaceUp = !pile.isFaceUp;
result += ". __Facing__ is changed to __" + $2 + "__.\n";
// Trigger the event of the pile changing
onPileChanged($1);
}
else
{
result += ". __Facing__ remains __" + (pile.isFaceUp ? "up": "down") + "__.\n";
}
// Update "Show moved" flag
if ($3 && pile.showMoved !== ($3 === "y"))
{
pile.showMoved = !pile.showMoved;
result += ". __ShowMoved__ is set to __" + pile.showMoved + "__.";
}
else
{
result += ". __ShowMoved__ remains __" + pile.showMoved + "__.";
}
return expFormat(result);
__ cards pilesettings {pile id: name text} {facing: up OR down, default: current} {show moved: y OR n, default: current} - Updates the settings for the {pile id} card-pile. - {facing} - Are the {pile id} card-pile's cards shown face-up or face-down? - {show moved} - Are cards that are drawn or picked into the {pile id} card-pile also printed to the note?
__
^cards? fromfolder ([_a-zA-Z][_a-zA-Z0-9]*) ("[^ \t\\:*?"<>|][^\t\\:*?"<>|]*"|[^ \t\\:*?"<>|]+)$
__
// Remove quotes around the path
$2 = $2.replace(/^"(.*)"$/, "$1")
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Cards not created. The __" + $1 + "__ card-pile was not found.");
}
const folder = app.vault.fileMap[$2];
// Early out if specified folder (to get cards from) doesn't exist or isn't a folder
if (!folder)
{
return expFormat("Cards not created. Folder __" + $2 + "__ was not found.");
}
if (!folder.children)
{
return expFormat("Cards not created. __" + $2 + "__ is not a folder.");
}
// Helper function - Get an image size by loading and reading its binary content
// Note - has a reliable, but slow backup function "getImageSize_reliable"
async function getImageSize_fast(file)
{
// Testing line
//file = app.vault.fileMap[file];
// Helper function - turn 4 bytes into an integer
function bytesToInt(v1, v2, v3, v4)
{
return (v1 << 24) + (v2 << 16) + (v3 << 8) + v4;
}
// Load the image's entire binary data (could optimize by not loading all)
const b = new Uint8Array(await app.vault.readBinary(file));
// Return value initialization
let result = null;
// If PNG file, read size
if (b[1] === 80 && b[2] === 78 && b[3] === 71)
{
result = [ bytesToInt(b[16], b[17], b[18], b[19]),
bytesToInt(b[20], b[21], b[22], b[23]) ];
}
// If GIF file, read size
else if (b[0] === 71 && b[1] === 73 && b[2] === 70)
{
result = [ bytesToInt(0, 0, b[7], b[6]),
bytesToInt(0, 0, b[9], b[8]) ];
}
// If BMP file, read size
else if (b[0] === 66 && b[1] === 77)
{
result = [ bytesToInt(b[21], b[20], b[19], b[18]),
bytesToInt(b[25], b[24], b[23], b[22]) ];
}
// If JPG file, read size
// Note - (JFIF) - https://stackoverflow.com/a/48488655 & further answers
else if (b[0] === 255 && b[1] === 216 && b[2] === 255 && b[3] === 224 &&
b[6] === 74 && b[7] === 70 && b[8] === 73 && b[9] === 70 && b[10]===0)
{
let i = 0;
while (i < b.length)
{
if (b[i] !== 255) { break; }
while (b[i] === 255) { i++; }
const mrk = b[i];
i++;
if(mrk === 1) { continue; } // TEM
if(208 <= mrk && mrk <= 215) continue;
if(mrk === 216) { continue; } // SOI
if(mrk === 217) { break; } // EOI
const len = bytesToInt(0, 0, b[i], b[i+1]);
if (192 <= mrk && mrk <= 207) // C0 to CF
{
i += 3;
result = [ bytesToInt(0, 0, b[i+2], b[i+3]),
bytesToInt(0, 0, b[i+0], b[i+1]) ];
break;
}
i += len;
}
}
// Return size, nicely formatted
return { w: result[0], h: result[1] };
}
// Helper function - Get image size by running image through html system
// NOTE - is a backup for the fast method
async function getImageSize_reliable(file)
{
let result = null;
try
{
// Read in the binary
const buf = await app.vault.readBinary(file);
// Convert binary to base64
let binary = '';
const bytes = new Uint8Array(buf);
bytes.forEach(b => binary += String.fromCharCode(b));
const base64Data = window.btoa(binary);
// Create image element, assign base-64 image to it, then read the size
result = await new Promise((onDone) =>
{
const i = new Image(1,1);
i.src = `data:${i.type};base64,${base64Data}`;
i.onload = () =>
{
onDone({ w: i.naturalWidth, h: i.naturalHeight });
}
i.onerror = () =>
{
onDone(null);
}
});
}
catch(e) {}
// Return the found image size
return result;
}
// Start expansion result
let result = "";
// Iterate over all children of the given folder
let createCount = 0;
for (let i = folder.children.length - 1; i >= 0; i--)
{
const childFile = folder.children[i];
// Ignore sub-folders
if (childFile.children) { continue; }
// Get image size
let size = await getImageSize_fast(childFile);
if (!size)
{
size = await getImageSize_reliable(childFile);
}
// If unable to get a card's size, it's not an image.
// Add notification to the expansion.
if (size == null)
{
result += "Unable to create card from file __" + childFile.name + "__.\n";
continue;
}
// Create and add the card object based on the current image
pile.cards.push(
{
path: childFile.path,
rotation: 0,
aspect: size.h / size.w,
origin: $1
});
createCount++;
}
// Trigger the event of the pile changing, if any cards were actually added
if (createCount)
{
onPileChanged($1);
}
return expFormat(
"__" + createCount + "__ cards added to the __" + $1 + "__ card-pile.");
__ cards fromfolder {pile id: name text} {folder: path text} - Creates cards based on images in {folder} and puts them into the {pile id} card-pile. - Note that this does not randomize the new cards. Call the "cards shuffle" shortcut to do that.
__
^cards? list?$
__
// Initialize expansion notification
let result = "Card-piles:\n";
// The collection of all piles
const piles = _inlineScripts.state.sessionState.cards.piles;
// List of all pile names
const names = Object.keys(piles);
// If there are piles, generate a list and add it to the expansion
if (names.length)
{
const list = names.map(v => v + " _(" + piles[v].cards.length + " cards)_");
result += ". " + list.join("\n. ");
}
// If there are NO piles, say so
else
{
result += "NONE";
}
return expFormat(result);
__ cards list - Lists all card-piles.
__
^cards? peek ?([1-9][0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|) ?(y|n|)$
__
// Early out if the pile to peek is not provided or remembered from a prior peek.
if (!$2 && !_inlineScripts.state.sessionState.cards.priorPeekPile)
{
return expFormat("Cards not peeked. The card-pile to peek into is undefined.");
}
// If peek-from-bottom isn't specified & not remembered from prior peek, use default.
if (!$3 && !_inlineScripts.state.sessionState.cards.priorPeekIsBottom)
{
$3 = "n";
}
// Set unspecified parameters to those from prior peek
if (!$2) { $2 = _inlineScripts.state.sessionState.cards.priorPeekPile; }
if (!$3) { $3 = _inlineScripts.state.sessionState.cards.priorPeekIsBottom; }
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$2];
if (!pile)
{
return expFormat(
"Cards not peeked. The card-pile __" + $1 + "__ was not found.");
}
// Remember the parameters for the next peek
_inlineScripts.state.sessionState.cards.priorPeekPile = $2;
_inlineScripts.state.sessionState.cards.priorPeekIsBottom = $3;
// Calculate actual values from the parameters
const fromBottom = ($3 === "y");
const count = Math.min(Number($1) || 1, pile.cards.length);
// Create view of peeked cards
let result = "";
for (let i = 0; i < count; i++)
{
const cardIndex = (fromBottom ? i : (pile.cards.length - i - 1));
result += createCardUi_inNote(true, pile.cards[cardIndex]);
}
return expFormat(
"Cards peeked from the " + (fromBottom ? "bottom" : "top") + " of the __" + $2 +
"__ card pile:\n" + result);
__ cards peek {count: >0, default: 1} {pile id: name text, default: prior} {from the bottom: y OR n, default: prior OR n} - Displays the first {count} cards in the {pile id} card-pile. If {from the bottom}, displays the LAST {count} cards instead.
__
^cards? draw ?([1-9][0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|)$
__
// Check if we should be using the OTHER "cards draw" shortcut
const $2Lowered = $2.toLowerCase();
if ($2Lowered === "from" || $2Lowered === "into" || $2Lowered === "to" &&
!_inlineScripts.state.sessionState.cards.piles[$2])
{
let otherWord = ($2Lowered === "from") ? "to" : "from";
return expand("cards draw " + $1 + " " + $2 + " " + $3 + " " + otherWord);
}
// Early out if source & destination piles not provided or recalled from prior draw.
if (!$2 && !_inlineScripts.state.sessionState.cards.priorDrawDst)
{
return expFormat("Cards not drawn. The destination card-pile is not defined.");
}
if (!$3 && !_inlineScripts.state.sessionState.cards.priorDrawSrc)
{
return expFormat("Cards not drawn. The source card-pile is not defined.");
}
// Set unspecified parameters to those from prior draw
if (!$2) { $2 = _inlineScripts.state.sessionState.cards.priorDrawDst; }
if (!$3) { $3 = _inlineScripts.state.sessionState.cards.priorDrawSrc; }
// Get the the source and destination piles
const dstPile = _inlineScripts.state.sessionState.cards.piles[$2];
const srcPile = _inlineScripts.state.sessionState.cards.piles[$3];
// Early out if the source or destination piles don't exist
if (!dstPile)
{
return expFormat(
"Cards not drawn. The destination card-pile __" + $2 + "__ was not found.");
}
if (!srcPile)
{
return expFormat(
"Cards not drawn. The source card-pile __" + $3 + "__ was not found.");
}
// Remember the parameters for the next draw
_inlineScripts.state.sessionState.cards.priorDrawDst = $2;
_inlineScripts.state.sessionState.cards.priorDrawSrc = $3;
// Calculate actual values from the parameters and settings
const count = Math.min(Number($1) || 1, srcPile.cards.length);
// Calculate whether to show the drawn cards in the expansion
const showCards = (dstPile.showMoved && dstPile.isFaceUp);
// Pull each card to draw (and make a display of them if "showCards" is true)
let drawnCount = 0;
let cardView = showCards ? "\n" : "";
let transfer = [];
for (let i = 0; i < count; i++)
{
if (!srcPile.cards.length) { break; }
const drawnCard = srcPile.cards.pop();
transfer.push(drawnCard);
drawnCount++;
cardView += showCards ? createCardUi_inNote(dstPile.isFaceUp, drawnCard) : "";
}
// Add each drawn card to the destination pile
transfer.reverse();
dstPile.cards.push(...transfer);
// Make the expansion string
const faceDownMsg = dstPile.isFaceUp ? "" : " _(face-down)_";;
const result =
"__" + drawnCount + "__ card(s) drawn from the __" + $3 +
"__ card-pile to the __" + $2 + "__ card-pile" + faceDownMsg + "." + cardView;
// Trigger the events of the source and destination piles changing
onPileChanged($2);
onPileChanged($3);
return expFormat(result);
__ cards draw {count: >0, default: 1} {destination pile id: name text, default: prior} {source pile id: name text, default: prior} - Removes {count} cards from the {source pile id} card-pile and adds them to the {destination pile id} card-pile.
__
^cards draw (.*)$
__
// Split the details parameter into detail words to parse
const detailWords = $1.split(" ").filter(v => v);
// Setup the "from" and "to" to pull from the details
let fromPileName = "";
let toPileName = "";
let count = "";
// Track the expected next detail word's type based on the prior detail word
let expectation = 0;
// Iterate over all detail words
for (const detailWord of detailWords)
{
// Use the lowercase version of the word for case insensitivity
const detailWordLowered = detailWord.toLowerCase();
// Handle logic for the current detail word based on expectation
switch (expectation)
{
case 0:
if (detailWordLowered === "from")
{
expectation = 1;
}
else if (detailWordLowered === "into" ||
detailWordLowered === "to")
{
expectation = 2;
}
else if (Number(detailWord) && Number(detailWord) > 0)
{
count = detailWord;
}
else
{
return expFormat(
[ "", "Cards not picked. Expected 'from', 'into', 'to' or a " +
"positive number, but found __" + detailWord + "__." ]);
}
break;
case 1:
fromPileName = detailWord;
expectation = 0;
break;
case 2:
toPileName = detailWord;
expectation = 0;
break;
}
}
return expand("cards draw " + count + " " + toPileName + " " + fromPileName);
__ cards draw {details: text} - This version of "draw" lets you define the 3 parameters in your own order.
- Parameters:
- source card-pile - prefix with the word "from". If skipped, defaults to what it was on the prior draw.
- destination card-pile - prefix with the word "into" or "to". If skipped, defaults to what it was on the prior draw.
- count - don't prefix with anything. It'll be recognized as a number of 1 or more. If skipped, defaults to 1.
- Examples:
;;cards draw 3 from deck into hand::
;;cards draw to hand 5 from deck::
;;cards draw into discard::
;;cards draw 7 from deck::
;;cards draw::
__
^cards? pick ?([_a-zA-Z][_a-zA-Z0-9]*|) ?([_a-zA-Z][_a-zA-Z0-9]*|)$
__
// Check if we should be using the OTHER "cards pick" shortcut
const $1Lowered = $1.toLowerCase();
if ($1Lowered === "from" || $1Lowered === "into" || $1Lowered === "to" &&
!_inlineScripts.state.sessionState.cards.piles[$1])
{
let otherWord = ($1Lowered === "from") ? "to" : "from";
return expand("cards pick " + $1 + " " + $2 + " " + otherWord);
}
// Early out if source & destination piles not provided or recalled from prior pick.
if (!$1 && !_inlineScripts.state.sessionState.cards.priorPickDst)
{
return expFormat("Cards not picked. The destination card-pile is not defined.");
}
if (!$2 && !_inlineScripts.state.sessionState.cards.priorPickSrc)
{
return expFormat("Cards not picked. The source card-pile is not defined.");
}
// Set unspecified parameters to those from prior pick
if (!$1) { $1 = _inlineScripts.state.sessionState.cards.priorPickDst; }
if (!$2) { $2 = _inlineScripts.state.sessionState.cards.priorPickSrc; }
// Get the the source and destination piles, early out if either doesn't exist
const dstPile = _inlineScripts.state.sessionState.cards.piles[$1];
const srcPile = _inlineScripts.state.sessionState.cards.piles[$2];
if (!dstPile)
{
return expFormat(
"Cards not picked. The destination card-pile __" + $1 +
"__ was not found.");
}
if (!srcPile)
{
return expFormat(
"Cards not picked. The source card-pile __" + $2 + "__ was not found.");
}
// Remember the parameters for the next pick
_inlineScripts.state.sessionState.cards.priorPickDst = $1;
_inlineScripts.state.sessionState.cards.priorPickSrc = $2;
// Pick cards
let picks = popups.custom(
"Pick cards to move from the <b>" + $2 + "</b> to the <b>" + $1 + "</b>.",
_inlineScripts.cards.cardPickerPopup, { pileId: $2 });
if (!picks) { return; }
// Calculate whether to show the picked cards in the expansion
const showCards = (dstPile.showMoved && dstPile.isFaceUp);
// Pull each picked card (and make a display of them if "showCards" is true)
let cardView = showCards ? "\n" : "";
let transfer = [];
for (let i = 0; i < picks.length; i++)
{
if (!srcPile.cards.length) { break; }
const drawnCard = srcPile.cards.splice(picks[i], 1)[0];
transfer.push(drawnCard);
cardView += showCards ? createCardUi_inNote(dstPile.isFaceUp, drawnCard) : "";
}
// Add each picked card to the destination pile
transfer.reverse();
dstPile.cards.push(...transfer);
// Make the expansion string
const faceDownMsg = dstPile.isFaceUp ? "" : " _(face-down)_";
const result =
"__" + picks.length + "__ card(s) picked from the __" + $2 +
"__ card-pile to the __" + $1 + "__ card-pile" + faceDownMsg + "." + cardView;
// Trigger the events of the source and destination piles changing
onPileChanged($1);
onPileChanged($2);
return expFormat(result);
__ cards pick {destination pile id: name text, default: prior} {source pile id: name text, default: prior} - Has the user choose cards from the {source pile id} card-pile. Moves the chosen cards into the {destination pile id} card-pile.
__
^cards pick (.*)$
__
// Split the details parameter into detail words to parse
const detailWords = $1.split(" ").filter(v => v);
// Setup the "from" and "to" to pull from the details
let fromPileName = "";
let toPileName = "";
// Track the expected next detail word's type based on the prior detail word
let expectation = 0;
// Iterate over all detail words
for (const detailWord of detailWords)
{
// Use the lowercase version of the word for case insensitivity
const detailWordLowered = detailWord.toLowerCase();
// Handle logic for the current detail word based on expectation
switch (expectation)
{
case 0:
if (detailWordLowered === "from")
{
expectation = 1;
}
else if (detailWordLowered === "into" ||
detailWordLowered === "to")
{
expectation = 2;
}
else
{
return expFormat(
[ "", "Cards not picked. Expected 'from', 'into' or 'to', " +
"but found __" + detailWord + "__." ]);
}
break;
case 1:
fromPileName = detailWord;
expectation = 0;
break;
case 2:
toPileName = detailWord;
expectation = 0;
break;
}
}
return expand("cards pick " + toPileName + " " + fromPileName);
__ cards pick {details: text} - This version of "pick" lets you define the 2 parameters in your own order.
- Parameters:
- source card-pile - prefix with the word "from". If skipped, defaults to what it was on the prior pick.
- destination card-pile - prefix with the word "into" or "to". If skipped, defaults to what it was on the prior pick.
- Examples:
;;cards pick from deck into hand::
;;cards pick to hand from deck::
;;cards pick into discard::
;;cards pick from deck::
;;cards pick::
__
^cards? shuffle ([_a-zA-Z][_a-zA-Z0-9]*) ?(y|n|)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Cards not shuffled. The __" + $1 + "__ card-pile was not found.");
}
// Randomize order of cards in pile
for (let i = pile.cards.length - 1; i > 0; i--)
{
const j = Math.floor(Math.random() * (i + 1));
[pile.cards[i], pile.cards[j]] = [pile.cards[j], pile.cards[i]];
}
// Randomize rotation of cards in pile
if ($2 === "y")
{
for (let card of pile.cards)
{
card.rotation =
(card.aspect == 1) ?
Math.trunc(Math.random() * 4) :
Math.trunc(Math.random() * 2) * 2;
}
}
// Trigger the event of the pile changing
onPileChanged($1);
return expFormat("The __" + $1 + "__ card-pile is shuffled.");
__ cards shuffle {pile id: name text} {rotate: y OR n, default: n} - Randomizes the card order for the {pile id}. If {rotate}, then card rotations are also randomized. - Rotation typically means 0 or 180 degrees (right-side-up or up-side-down), but can also mean 90 or 270 degrees if the card is square.
__
^cards? unrotate ([_a-zA-Z][_a-zA-Z0-9]*)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Cards not unrotated. The __" + $1 + "__ card-pile was not found.");
}
// Unrotate all cards in the pile
let unrotatedCount = 0;
for (let card of pile.cards)
{
if (card.rotation !== 0)
{
card.rotation = 0;
unrotatedCount++;
}
}
// Trigger the event of the pile changing, if any of the cards were unrotated
if (unrotatedCount)
{
onPileChanged($1);
}
return expFormat(
"The __" + $1 + "__ card-pile's cards are all rotated to 0 degrees " +
"(right-side-up).");
__ cards unrotate {pile id: name text} - Sets the rotation for all cards in the {pile id} card-pile to 0 (right-side-up).
__
^cards? reverse ([_a-zA-Z][_a-zA-Z0-9]*)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Cards not reversed. The __" + $1 + "__ card-pile was not found.");
}
// Reverse the order of the cards
pile.cards.reverse();
// Trigger the event of the pile changing
onPileChanged($1);
return expFormat("Cards in the __" + $1 + "__ card-pile have been reversed.");
__ cards reverse {pile id: name text} - Reverses the order of the cards in the {pile id} card-pile.
__
^cards? recall ([_a-zA-Z][_a-zA-Z0-9]*)$
__
// Get the list of piles and the specified pile, early out if it doesn't exist
const piles = _inlineScripts.state.sessionState.cards.piles;
const pile = piles[$1];
if (!pile)
{
return expFormat(
"Cards not recalled. The __" + $1 + "__ card-pile was not found.");
}
// Iterate over all piles, pulling all cards with an origin of the specified pile
let recallCount = 0;
let transfer = [];
for (const key in piles)
{
if (piles[key] === pile) { continue; }
// Iterate over cards in this pile, checking their origin and pulling if a match
for (let k = piles[key].cards.length - 1; k >= 0; k--)
{
if (piles[key].cards[k].origin === $1)
{
transfer.push( piles[key].cards.splice(k, 1)[0] );
recallCount++;
}
}
}
// Put all pulled cards into the specified pile
transfer.reverse();
pile.cards.push(...transfer);
// Trigger the event of all piles changing, if any cards were recalled
if (recallCount)
{
onPileChanged(null);
}
return expFormat(
"__" + recallCount + "__ cards returned to the __" + $1 + "__ card-pile.");
__ cards recall {pile id: name text} - Moves all cards that have the {pile id} card-pile as their origin, from their current card-piles back into the {pile id} card-pile. If {facing} is specified, all cards in {pile id} are then put to face {facing}.
__
^cards? reorigin ([_a-zA-Z][_a-zA-Z0-9]*)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Cards not re-origined. The __" + $1 + "__ card-pile was not found.");
}
// Iterate over all cards in the pile and set their origin to the pile
for (let card of pile.cards)
{
card.origin = $1;
}
return expFormat("Cards in the __" + $1 + "__ card-pile have been re-origined.");
__ cards reorigin {pile id: name text} - Sets the origin of all cards in the {pile id} card-pile to {pile id}.
__
^cards? import ([_a-zA-Z][_a-zA-Z0-9]*) (.+)$
__
// Early out if the specified pile doesn't exist
if (!_inlineScripts.state.sessionState.cards.piles[$1])
{
return expFormat(
"Card-pile not imported. The __" + $1 + "__ card-pile was not found.");
}
// Try and parse the given data. Early out if failed
let data = null;
try
{
data = JSON.parse($2);
if (!Array.isArray(data.cards))
{
throw null;
}
}
catch (e)
{
return expFormat("Card-pile not imported. Failed to parse data-string.");
}
// Set the specified pile to the given data
_inlineScripts.state.sessionState.cards.piles[$1] = data;
// Trigger the event of the specified pile changing
onPileChanged($1);
return expFormat("The __" + $1 + "__ card-pile was imported");
__ cards import {pile id: name text, default: table} {data: text} - Imports the {data} into the {pile id} card pile.
__
^cards? export ([_a-zA-Z][_a-zA-Z0-9]*)$
__
// Get the pile, early out if it doesnt exist
const pile = _inlineScripts.state.sessionState.cards.piles[$1];
if (!pile)
{
return expFormat(
"Card-pile not exported. The __" + $1 + "__ card-pile was not found.");
}
// Make a data-string from the specified pile
let result = JSON.stringify(pile);
// Return the data-string
// NOTE: don't use the expansion-format's prefix or line-prefix. This makes the
// data-string is easier to select and copy in the note.
return expFormat("Card-pile __" + $1 + "__ export:\n" + result + "", true, true);
__ cards export {pile id: name text} - Expands to a data-string containing all date for the {pile id} card-pile.