Other Ways to Access Documents
In the previous article, we learned how to access documents by their ID or UUID. However, there are other ways to access documents.
Example case:
You need a macro that needs to access a specific item, but the item in question might be on multiple actors, and you want a macro that works for all of them.
Accessing Documents by Name
While IDs and UUIDs are unique to a document, names are not. For example, you could have multiple actors with the same name. Keep in mind that when using the getName
method, the first document with the name will be returned.
const actorName = "My Actor";
const actorDocument = game.actors.getName(actorName);
console.log(actorDocument);
Example: Changing the Image of a Specifically Named Item
Let's return to our previous example of accessing a specific item by its name.
In this example, we will create a macro that checks if a specific item is present on our currently selected token's actor, and if it is, it will change the image of the item to a different one.
const itemName = "My Item";
const itemDocument = token.actor.items.getName(itemName);
if (itemDocument) {
itemDocument.update({ "img": "my/new/image.webp" });
}
Before We Continue: JavaScript Arrays, Array Methods, and Loops
In JavaScript, an array is a collection of values stored in a single variable. Arrays are used to store multiple values in a single variable, and they can be accessed using indexes or looped through. They are distinguishable by their square brackets []
.
Example:
const myArray = [1, 2, 3, 4, 5];
console.log(myArray[0]); // Output: 1
console.log(myArray[2]); // Output: 3
console.log(myArray.length); // Output: 5
You can add values to an array using the push
method.
Example:
const myArray = [1, 2, 3, 4, 5];
myArray.push(6);
console.log(myArray); // Output: [1, 2, 3, 4, 5, 6]
Finding All Documents with a Specific Name
As we saw in the previous example, you can use the getName
method to find a document by its name. However, if you have multiple documents with the same name, the getName
method will return the first document with that name.
To find all documents with a specific name, we will need to use array methods.
Example:
const itemName = "My Item";
let itemDocuments = []; // Create an empty array to store the documents
for (const item of game.items) {
if (item.name === itemName) {
itemDocuments.push(item); // Push the item to the array if it matches the name
}
}
console.log(itemDocuments); // Output: [item1, item2, item3]
JavaScript For Loops
We have already seen conditions and arrays, but this is the first time we have seen loops. Loops are used to repeat a block of code multiple times.
We can either iterate a number of times or iterate over an array, and JavaScript will automatically iterate over each element in the array.
Example: Iterating over an array
for (const item of game.items) {
console.log(item);
}
// Output: item1, item2, item3
Example: Iterating over a number
for (let i = 0; i < 5; i++) {
console.log(i);
}
// Output: 0, 1, 2, 3, 4
Note that there are more advanced ways to work with arrays, but they are not more performant or readable than a basic for loop. For this reason, we will not cover them in this guide. Once you have familiarized yourself with for loops and arrays, you can read more about arrays and their methods if you wish. If you wish to learn more about JavaScript arrays and array methods, you can check out the MDN Array Docs (opens in a new tab).
Manipulating Multiple Documents at Once
Now that we know how to work with loops and arrays, we can use them to manipulate multiple documents at once.
Example: Changing the Image of All Tokens on the Scene with a Specific Name
We know how to get all documents with a specific name and how to perform updates. Let's combine them to make a macro that will change the image of all tokens on the scene with a specific name.
const tokenName = "My Token";
for (const tokenDocument of canvas.scene.tokens) {
if (tokenDocument.name === tokenName) {
await tokenDocument.update({ "texture.src": "my/new/image.webp" });
}
}
You can also nest loops. Let's say you want to change the image of all tokens on all scenes with a specific name.
const tokenName = "My Token";
for (const scene of game.scenes) {
for (const tokenDocument of scene.tokens) {
if (tokenDocument.name === tokenName) {
await tokenDocument.update({ "texture.src": "my/new/image.webp" });
}
}
}
As a last example, let's turn off all lights on the current scene.
for (const lightDocument of canvas.scene.lights) {
await lightDocument.update({ hidden: true });
}
A Note on await
As you can see in the examples above, we are using the await
keyword to wait for the update to complete before moving on to the next document. This is not necessary in this case as every update is independent of each other, but it is good practice to use await
when you are working with asynchronous code.
Advanced: Batching Updates
If you are working with a lot of documents, you can batch updates to improve performance. In most use cases, this is not particularly relevant and will be optimization for optimization's sake. However, it's good practice to use batch updates when you are working with multiple documents.
It's important to note that batch updates can only be done within the same Collection
. For example, you can't batch update a token and an item at the same time, and you can't update all tokens across all scenes with a single update.
Collections
In Foundry a collection is a data structure that contains a set of documents. Things such as game.actors
, game.items
, game.scenes
, and game.journals
are all collections. You can find a list of all of them in game.collections
.
While things such as Actor
are not a collection, they are documents which can have other documents embedded into them as collections, for example Actor.items
is a collection of items that belong to that actor.
Batch Update Syntax
Batch updates are done using the updateEmbeddedDocuments
method.
canvas.scene.updateEmbeddedDocuments("Token", [updateData]);
The first parameter is the type of document we are updating, in this case, it's Token
. The second parameter is an array of objects containing the data we want to update.
If you are unsure of the type of document you want to update, you can log it in the console and check the type.
console.log(token.document.documentName);
Let's review our previous examples, but this time we will use batch updates.
Batch Update Examples
Let's say we want to change the image of all tokens on the current scene.
const tokenName = "My Token";
const updateData = [];
for (const tokenDocument of canvas.scene.tokens) {
if (tokenDocument.name === tokenName) {
updateData.push({ _id: tokenDocument.id, "texture.src": "my/new/image.webp" });
}
}
await canvas.scene.updateEmbeddedDocuments("Token", updateData);
As you can see, we are using the _id
property to identify the document we want to update. We are also using the updateEmbeddedDocuments
method to update the document.
If we want to change the image of all tokens on all scenes, we can use the same method but we will need to iterate over all scenes with a separate updateEmbeddedDocuments
call for each scene.
const tokenName = "My Token";
for (const scene of game.scenes) {
const updateData = [];
for (const tokenDocument of scene.tokens) {
if (tokenDocument.name === tokenName) {
updateData.push({ _id: tokenDocument.id, "texture.src": "my/new/image.webp" });
}
}
await scene.updateEmbeddedDocuments("Token", updateData);
}
With this knowledge, you should now be able to perform all sorts of mass updates on your world. As it's your first time, it's highly recommended to perform a backup of your world before you start making changes.