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.