World Scripts, similar to Modules and Macros, are a way for users to input additional functionality into Foundry VTT.
Differences
Macros
Macros in general are small, standalone pieces of code that are meant to only run once, when prompted and only on the side of the user triggering the Macro. As a rule of thumb, core Foundry VTT’s Macros cannot “react” or “run when X” and generally are not allowed to run code on other users (unless assisted by the System or Modules)
Modules
Modules are very similar to World Scripts, as both allow custom JavaScript files to be loaded and executed by Foundry. The main difference is that one is attached to a Module and the other is attached to a World. Because of this, learning resources designed for modules are often also applicable to world scripts.
Unless you specifically want to add always-on code to one world only, you should instead add your scripts to a local module. There is a full guide for this made by GamerFlix.
Setup
To create a working World Script, you need to:
- Modify the World’s manifest file to tell Foundry to load that JS file.
- Create a JavaScript file(s) in your World’s folder.
Creating a JavaScript file and Modifying World’s manifest file
To include a JavaScript file in your world, perform the following steps:
- Shut down Foundry
- Go to your World’s folder (
Data/worlds/your-world/
) in your Foundry User Data folder - Open the
world.json
file. This is your World’s manifest. It contains all important configuration data about your World package. - In your manifest, look for a line with
esmodules
key. If there isn’t any, insert a new line with the following text:"esmodules": ["your-script.mjs"],
anywhere (good place is for example right after line withid
). The first five lines of the file could look, for example, like this:{ "title": "Your World", "id": "your-world", "esmodules": ["your-script.mjs"], "system": "wfrp4e",
- Please note that the comma is required at the end of every entry in JSON, except the last. If in doubt, use any available online “JSON Validators” to make sure your file is a valid JSON.
- Save and close
world.json
. - In the same folder create a file and name it
your-script.mjs
. Make sure that its full name and extension are
exactly matching the one that you added in step 4. - Launch Foundry and launch your world.
Every time you change anything about the manifest file, you should fully restart the Foundry server.
If you use Foundry App, that means restarting the App, if you use node.js, that means stopping and restarting the process.
The actual name of your JavaScript file does not really matter, as long as it matches the path supplied in step 4. Extension could be either
.js
or.mjs
– it’s customary to use.js
for scripts and.mjs
for ESModules, but both will work just fineAvoid spaces and special characters in your file names. Best practices often suggest keeping files named in
snake-case
for scripts andStudlyCaps
for classes.Common issue on Windows is that your file might in reality have
.mjs.txt
extension, so make sure to watch out for that
Congratulations, you now have a World Script that will be loaded and executed for every connecting user once upon login!
Let’s test it!
- Open your JavaScript file and add the following inside:
alert("Hello World!");
- Save the file and refresh the World (press F5,
CTRL+R
or use any other way of refreshing your browser) - You should be greeted by an alert popup. Now remove it so you don’t annoy your players ;)
Examples
Item Availability
How can I add more Item availabilities?
Hoks.once("init", () => {
game.wfrp4e.config.availability = foundry.utils.mergeObject(game.wfrp4e.config.availability, {
legendary: "Legendary",
unique: "Unique"
});
});
Test Difficulties
How can I add custom Test difficulties?
Hoks.once("init", () => {
const config = {
difficultyModifiers: {},
difficultyLabels: {},
};
config.difficultyModifiers["godly"] = 100;
config.difficultyModifiers["ungodly"] = -100;
config.difficultyLabels["godly"] = "Godly (+100)";
config.difficultyLabels["ungodly"] = "Ungodly (–100)";
game.wfrp4e.config = foundry.utils.mergeObject(game.wfrp4e.config, config);
});
Alternative XP Cost per Advance
I personally believe the 6th Skill Advancement should cost 10 XP because Actor currently having 5 Advances looks at the first line in XP Cost per Advance
table. System, however, calculates 15 XP. How can I change that?
function myCalculateAdvCost(currentAdvances, type, modifier = 0) {
let index = (Math.ceil(currentAdvances / 5) - 1);
index = Math.max(0, index);
if (index >= game.wfrp4e.config.xpCost[type].length)
return game.wfrp4e.config.xpCost[type][game.wfrp4e.config.xpCost[type].length - 1] + modifier;
return game.wfrp4e.config.xpCost[type][index] + modifier;
}
Advancement.calculateAdvCost = myCalculateAdvCost;
Merging Hooks
Examples that require Hooks are usually shown wrapped in their own
Hooks.once("init", () => { code here });
so they can be copied and pasted in a working condition.While it’s not necessarily wrong to have multiple registered Hooks, sometimes it is cleaner to keep everything merged under a single Hook.
Using the above examples of Availability and Test Difficulties, merged Hook could look like this:
Hoks.once("init", () => {
const config = {
availability: {},
difficultyModifiers: {},
difficultyLabels: {},
};
config.availability = {
legendary: "Legendary",
unique: "Unique"
};
config.difficultyModifiers["godly"] = 100;
config.difficultyModifiers["ungodly"] = -100;
config.difficultyLabels["godly"] = "Godly (+100)";
config.difficultyLabels["ungodly"] = "Ungodly (–100)";
game.wfrp4e.config = foundry.utils.mergeObject(game.wfrp4e.config, config);
});
or like this:
function registerAvailability() {
game.wfrp4e.config.availability = foundry.utils.mergeObject(game.wfrp4e.config.availability, {
legendary: "Legendary",
unique: "Unique"
});
}
function registerDifficulties() {
const config = {
difficultyModifiers: {},
difficultyLabels: {},
};
config.difficultyModifiers["godly"] = 100;
config.difficultyModifiers["ungodly"] = -100;
config.difficultyLabels["godly"] = "Godly (+100)";
config.difficultyLabels["ungodly"] = "Ungodly (–100)";
game.wfrp4e.config = foundry.utils.mergeObject(game.wfrp4e.config, config);
}
Hooks.once("init", () => {
registerAvailability();
registerDifficulties();
});