Recipe Book
A module for Foundry VTT that adds a recipe book to make automatic item crafting easier.
This project is developed independently, in the developer's free time. Suggestions, bug reports, and fixes are always welcome.
How to use
Game Master
- A Recipe Book button appears at the top of the sidebar's Items tab, above "Create Item"/"Create Folder". Click it to open the book.
- Click New Recipe: give it a name, tags (comma-separated, optional), and a description.
- Drag items (from a compendium or the world's item directory) into the Ingredients box and the Products box. Adjust quantities in the number fields, or drag the same item again to increase the required amount.
- Check which characters receive this recipe, under Assign to characters (grouped by owning player).
- Save. The recipe now appears in the book of the players who own those characters, grouped by the tags you defined.
- The Crafting Window can be opened/closed in two ways:
- Clicking the lock icon at the top of the recipe list, inside the book itself.
- Through the control integrated into Foundry's player list (the panel in the bottom-left corner showing who's online).
Player
- Open the Recipe Book from the Items tab. If the player has no character of their own, the book won't open — a warning appears asking the GM to assign a character to them.
- At the top of the list, a button shows the currently selected character. If the player has more than one character, clicking it opens a list to switch. The book shows the recipes assigned to the selected character.
- If the Crafting Window is open and the selected character has the required ingredients, the Craft button becomes available. Clicking it consumes the ingredients and the product(s) appear in the character's inventory, with a chat message logging the craft.
- If an ingredient is missing or the window is closed, the game explains why and nothing is consumed.
Managing assignments
The Manage Assignments button (icon of three people, next to the Crafting Window lock icon, GM only) opens a window listing every recipe grouped by player instead of by recipe:
- A recipe with characters from more than one player appears once under each of those players.
- Recipes with no character assigned at all are grouped under Unassigned, at the end of the list.
- Clicking a recipe's name expands the same character checklist used in the recipe editor, letting the GM assign or unassign characters directly from this window.
- Assign All / Unassign All, at the top of the window, apply to every recipe at once — you pick a specific character or "All characters" when prompted.
Game system compatibility
The module is system-agnostic. The only system-specific setting is the item quantity field (Configure Settings > Recipe Book), which defaults to system.quantity (used by most systems, including D&D5e). If your system stores quantity in a different field, adjust this value. (The Symbaroum system, for example, uses the system.number field.) Systems where items have no quantity work normally — the module treats a missing field as a quantity of 1 per item.
Importing, exporting, and deleting recipes
Meant to separate the crafting engine (this module) from content specific to each game system (items and recipes), avoiding broken items/recipes caused by system incompatibility. The idea: each game system gets its own content module, with an Item Compendium Pack (or world items) and a recipes file pointing to those items via UUID.
Three buttons are available under Configure Settings > Recipe Book, GM only:
- Export: generates a
recipe-book-recipes.jsonfile with every recipe in this world, ready to import into another world, merge with another file, or serve as the base for a content module. - Import: asks for the path to a JSON file (with a browse button, same as the recipe editor's image picker) and imports the recipes in it. Can be a file someone sent you, one you exported yourself, or one inside an installed module — the module never scans anywhere automatically, it only looks at the path you give it.
- Delete All: permanently removes every recipe in this world, after a confirmation step. Cannot be undone.
File format (the same one both Export produces and Import expects):
[
{
"id": "barrvalgs-cauldron",
"name": "Barrvalg's Cauldron",
"tags": ["Potions"],
"description": "<p>Recipe description, in HTML.</p>",
"ingredients": [
{ "uuid": "Compendium.my-content-module.my-items.XXXXXXXX", "name": "Blue Drops", "quantity": 1 }
],
"results": [
{ "uuid": "Compendium.my-content-module.my-items.YYYYYYYY", "name": "Barrvalg's Cauldron", "quantity": 1 }
]
}
]
- A plain array of recipes — no wrapper object, which makes it easy to merge two exports into a single file (just combine the arrays).
idis optional on import, but recommended — used to recognize the recipe on future imports and avoid duplicates. If omitted, it's derived fromname. The Export button always includes a stableid.nameinsideingredients/resultsexists only to make the file human-readable and easier to merge by hand — on import, the real name and image are always re-read from the actual item (viauuid); whatever is written innamein the file is never trusted.assignedActorIdsis not part of the format — assigning characters is always done by the GM, inside the world, after importing.- If an ingredient/product references a
uuidthat doesn't exist (pack not installed, or installed for the wrong system), that entire recipe is skipped — a recipe is never created with fewer ingredients/products than originally defined.
For content module developers: the settings Import button is meant for occasional, manual use (one person importing a standalone file). A content module distributing its own recipes should call the API directly, instead of relying on the GM clicking anything in Recipe Book's settings. Recommended: register your own settings menu (with an "Import"/"Update" button) instead of importing automatically on Hooks.once("ready") — that way players don't pay the cost of that check every time the world loads, and you control when a content update is applied. See recipe-book-content-template (example module) for a ready-made model of this pattern, with two buttons: one that only adds what's missing, and one that restores everything (overwriting what already exists in the world).
const recipeBook = game.modules.get("recipe-book");
if (!recipeBook?.active) return;
const response = await fetch("modules/my-content-module/recipe-book-recipes.json");
const recipes = await response.json();
const result = await recipeBook.api.importRecipes(recipes, { source: "my-content-module", overwrite: false });
console.log(`Recipe Book: ${result.imported} imported, ${result.updated} updated, ${result.skipped} already existed.`, result.errors);
sourceshould be a stable identifier for the content module (the module's ownidworks well). It's combined with each recipe'sidto form the import identifier — reimporting withoutoverwritewon't duplicate or overwrite recipes already imported before. The reasons for any skipped recipe end up inresult.errors.overwrite: false(default) skips recipes already imported before, without touching them.overwrite: trueupdates already-imported recipes with the file's data (name, tags, description, ingredients, products), but never touchesassignedActorIds— that's always the GM's choice, made inside the world.
Adding a new language
This was designed to be quite simple:
- Copy
lang/en.json(orlang/pt-BR.json) to a new file, e.g.lang/es.json. - Translate the values (the part to the right of each
:). Do not change the keys on the left (e.g."RECIPE-BOOK.App.Title") — only the translated text. - Open
module.jsonand add an entry under"languages":
{ "lang": "es", "name": "Español", "path": "lang/es.json" }
- Restart Foundry (or reload the world). The new language will appear in Foundry's language options.
No other file needs to be touched to translate the module — all interface text comes from the files in lang/.
Module structure
recipe-book/
├── module.json Module manifest
├── scripts/
│ ├── constants.js Module ID
│ ├── debug.js Standardized console logging
│ ├── main.js Hooks, settings, settings menus, Items-tab button, player-list control
│ ├── recipe-data.js Recipe CRUD, import, and export
│ ├── crafting-logic.js Ingredient checking/consumption and item creation
│ ├── actor-groups.js Groups player characters by owner
│ └── apps/
│ ├── recipe-book-app.js Book window (GM + players)
│ ├── recipe-editor-app.js Recipe create/edit window (GM)
│ ├── recipe-assignments-app.js Manage Assignments window (GM)
│ ├── import-recipes-app.js Import Recipes window (settings menu)
│ ├── export-recipes-app.js Export Recipes window (settings menu)
│ └── delete-all-recipes-app.js Delete All Recipes window (settings menu)
├── templates/ Handlebars templates for the windows above
├── styles/recipe-book.css Styles
└── lang/
├── pt-BR.json Portuguese
└── en.json English
Technical notes
- Recipes are saved in a world setting (
recipe-book.recipes), so they're the same for all users and persist with the world. - Matching between a recipe's ingredient and the items a character owns is done in priority order: (1) exact same UUID, (2) same source UUID (item originated from a compendium/item via
flags.core.sourceId), (3) same item name, as a last resort. For greater reliability, prefer creating recipes by dragging items from a compendium or the world's item directory (not from individual character sheets), ensuring characters truly have items originating from there. - A simple API is exposed at
game.modules.get("recipe-book").api:RecipeBookApp,RecipeEditorApp,importRecipes,exportRecipes(see the import/export section above). To open the book from a macro, use:
new (game.modules.get("recipe-book").api.RecipeBookApp)().render(true);
Support my work
Images
