A couple of weeks ago, a user named idolofmanyhands on DM Scotty’s discord server created a Google Sheet called the EZD6 Repository, intended to be a shared community source for monsters and other resources created or adapted for that game. It already contains the contents of The Book of Beasts, an effort from another community member, Eric W – and will hopefully grow to include more community contributions.
My immediate thought on seeing this was “Hey, I’m pretty sure those Google Sheets can be queried from Javascript, which also happens to be the language used for macros in Foundry. I wonder how tough it would be to write something to import these monsters?”
Not very tough at all, as it turns out. A few hours over two evenings resulted in a successful import of all the monsters in the sheet!
What it Does
Running the macro will fetch data from the Repository and create a compendium called “EZD6 Repository Monsters” – or add to it if it already exists. Inside this compendium it looks at each line of the sheet and creates a monster actor for each one. Then it looks at the Features column and adds an appropriate item for each line, attempting to separate names from descriptions.
What it Doesn’t Do
It’s a simple importer and is limited by the data and formatting available, so it’s not perfect.
- There’s no attempt at managing multiple imports. If you run a second import without renaming or removing the compendium created by the first one, you’ll find it packed with a bunch of new items with the same names as existing ones.
- The macro is likely to break if the format of the spreadsheet changes significantly. I plan to attempt to post updated versions if this happens, but they likely won’t be immediate after a change.
- Magic resistance dice default to 1 unless there’s a “Magick resistance xD6” type item under features. If the text of entries describing magic resistance is formatted differently, it probably won’t pick them up. Spell ability is similar, defaulting to 0 unless there’s a “Magick Attack xD6” feature entry.
- Features can be a little messy. If there isn’t an obviously separate name and description, you can end up with really long feature names. If the formatting of the feature descriptions isn’t one per line, it can get a little confusing but is still workable. The definitely-not-a-trademarked-entity Watcher Fiend is an example of this.
- You’ll still need to find your own images and/or tokens.
Using the Macro
This script is added just like any other macro: Click an empty box in the macro bar at the bottom of the Foundry screen to open the New Macro editor. Give it a name, set the Type to Script, copy the code from the box below, and paste it into the Command box. Then click Save Macro.
Clicking on the new item in the toolbar runs the macro. It should create the new compendium and begin populating it; how long it takes to finish will depend on your internet speed and the number of items in the sheet, but it’s not pulling in large amounts of data so it shouldn’t be long. I didn’t time it, but for the 59 or so monsters currently in the Repository it took me only 10 seconds or so to create everything.
The Code
Copy the entire code below and paste it into the macro editor in Foundry.
// Change this if you want a different compendium name
// No effort is made to manage duplicates, etc., so change this or delete
// old compendium of same name before re-running
const compendiumName = "EZD6 Repository Monsters";
// These might need to change from time to time
const sheetID = "1en5zxbAwOKBQ2aK-lwvved4dzeiTJ0NPYp4PnpLbIUk";
const sheetName = "Bestiary";
const query = "SELECT *";
// Generally leave anything below here alone
const base = `https://docs.google.com/spreadsheets/d/${sheetID}/gviz/tq?`;
const url = `${base}&sheet=${encodeURIComponent(
sheetName)}&tq=${encodeURIComponent(query)}`;
const packName = "world." + compendiumName.slugify();
// first find or create the compendium
var compendium = await(game.packs.get(packName));
if (compendium == null) {
compendium = await CompendiumCollection.createCompendium({
label: compendiumName,
type: 'Actor',
private: true,
packageType: 'world',
});
}
var resp = await fetch(url)
.then((res) => res.text())
.then(function (response) {
var responseText = response.substring(response.indexOf("(") + 1, response.lastIndexOf(")"));
return JSON.parse(responseText).table.rows;
});
for (const row of resp) {
let actor = await Actor.create({
name: row["c"][0]["v"],
type: "monster",
system: {
tohit: ((row["c"][3] != null) ? row["c"][3]["v"] : 0),
description: row["c"][1]["v"] + "<br /><br />Source: " + row["c"][5]["v"],
strikes: {
value: ((row["c"][2] != null) ? row["c"][2]["v"] : 0),
max: ((row["c"][2] != null) ? row["c"][2]["v"] : 0),
},
},
}, {
pack: packName
});
if (row["c"][4] != null) {
for (var feature of row["c"][4]["v"].split("\n")) {
feature = feature.replace("• ", ""); // get rid of bullet points
const details = feature.split(" - "); //split name from description
if (details[0].replace(/ /g, '') != "") {
Item.create({
name: details[0],
type: "monsterfeature",
system: {
description: ((details.length == 2) ? details[1] : null),
}
}, {
parent: actor
});
// check for magick attack or resist
feature = feature.toLowerCase();
if (feature.toLowerCase().startsWith("magick resistance")) {
let mr = feature.substring(18,feature.indexOf("d")).trim();
mr = mr.split(" ")[0]; // handle exceptions with added descriptions
await actor.update({"system.magicresist": mr });
}
if (feature.toLowerCase().startsWith("magick attack")) {
let ma = feature.substring(13,feature.indexOf("d")).trim();
ma = ma.split(" ")[0]; // handle exceptions with added descriptions
await actor.update({"system.magicdice": ma });
}
}
}
}
}