Anjou's Blog

Building a Character Sheet in Foundry

Tags:

Fairly recently, I started exploring using the Foundry virtual tabletop software to run TTRPGs with my online friends. One big thing that drew me toward Foundry over Roll20 is that Foundry is willing to sell me a license as a one-time purchase rather than extracting subscription fees from me indefinitely. By the standards of the tech industry, that’s almost ethical!

If you’re using Foundry to host or play a game with one of the big, popular systems such as D&D 5th edition or Pathfinder, you’ll find lots of convenience features. I’ve found it to be a great experience! However, this post is not about using an existing system; it’s about my efforts learning how to adapt a new one to Foundry. And that has been a much more difficult experience.

In this post, I will describe the path from zero to a basic character sheet, including all the undocumented elements of Foundry I needed to get there.

Getting Started

Foundry’s knowledge base offers a number of guides for the aspiring system developer. Most relevant to me for this project, there’s an introduction to development, an introduction to system development, and an introduction to system data models. You may observe, as I did, that there are no guides beyond “introductions” to these topics, and these introductions do not take you far enough to create anything interactive. From here, you are encouraged to reference the API documentation as well as the enormous D&D 5e source code. The API documentation is generated from doc comments and… well, there’s quite a lot missing in there.

The System Manifest

Great news! This part is thoroughly explained in the official introduction to system development, so I’ll be glossing over any details not directly relevant to creating an editable character sheet.

normal  character-sheetsystem.json    1:20 json utf-8   unix  
{ "id": "my⁠-⁠test⁠-⁠system" , "title": "My Test System" , "description": "Toy system for exploring the creation of systems in Foundry" , "version": "0​.1​.0" , "url": "https://anjou​.wtf/example/foundry/system" , "manifest": "https://anjou​.wtf/example/foundry/system/latest/system​.json" , "download": "https://anjou​.wtf/example/foundry/system/v0​.1​.0/my⁠-⁠test⁠-⁠system​.zip" , "compatibility": { "minimum": 13 , "verified": 13 } , "esmodules": ["entrypoint​.mjs"] , "documentTypes": { "Actor": { "character": { "htmlFields": ["description"] } } } }

This is, more or less, the minimum you need to distribute your system in a way that lets users keep it up-to-date. The manifest should be a path to whatever the latest version of the manifest is, so that the updater knows when there’s a newer version than the one a user has installed, while download should be the path to the zip file for this specific version.

The documentTypes field is needed to declare your data model, which is how we’ll hook our own JavaScript class into the machinery of Actor creation. Here we declare a new type of Actor called a “character” which will be the type used for player characters. You don’t have to declare all the fields on your model, but you do need to declare certain special ones for data sanitizing purposes. If you want to store text with arbitrary (HTML) styling, you need to declare such fields as htmlFields the way I’ve done with the description field here. The same goes for storing files (such as images) with filePathFields. The documentation doesn’t tell you this, but you get an image field by default, so I do not need to include it in this example.

Defining a Data Model

Having declared our data model in the system manifest, we now need to define it in JavaScript. Here, as in the official guide, I’m extending the base TypeDataModel to define my own schema. In addition to the already-declared description field, I’m adding a SchemaField, which is just a container for multiple fields. In this toy system, when a player’s “toxicity” reaches a certain max threshold, they die.

normal  character-sheetentrypoint.mjs    1:32 js+foundry utf-8   unix  
1 const { HTMLField, SchemaField, NumberField, } = foundry.data.fields; 2 3 class CharacterData extends foundry.abstract.TypeDataModel { 4 static defineSchema() { 5 return { 6 description: new HTMLField({ required: false, label: "Biography" }), 7 toxicity: new SchemaField({ 8 value: new NumberField({ 9 required: true, 10 integer: true, 11 min: 0, 12 initial: 0, 13 label: "Toxicity", 14 }), 15 max: new NumberField({ 16 required: true, 17 integer: true, 18 min: 0, 19 initial: 8, 20 }) 21 }), 22 }; 23 } 24 25 get dead() { 26 return this.toxicity.value >= this.toxicity.max; 27 } 28 } 29 30 Hooks.on("init", () => { 31 CONFIG.Actor.dataModels.character = CharacterData; 32 });

Now we’ve got our custom character data model. In addition to the name and image we get for free by extending TypeDataModel, we’ve added a biography field that can hold HTML styling information as well as a compound toxicity field that represents both a current toxicity value and a maximum above which a character is declared dead. All that’s left is to design a character sheet.

Making the Character Sheet

At the time of this writing, configuring and coding a character sheet is an almost completely undocumented process. The guides left me with a data model that offers no UI for manipulating the data. In the rest of this post, I will be documenting everything I have learned through trial-and-error, copying snippets from dubious—and often broken—sources, reading code, and haranguing anyone who will listen. Wherever I am still unclear about the details, I will note it.

First, we need a new class to represent the character sheet UI. Anything that extends ActorSheetV2 will do, but Foundry’s built-in Handlebars integration needs a specific mix-in.

normal  character-sheetcharacter-sheet.mjs    1:6 js+foundry utf-8   unix  
const { ActorSheetV2 } = foundry.applications.sheets; const { HandlebarsApplicationMixin } = foundry.applications.api; class MyTestActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) {} export default MyTestActorSheet;

Having created it, you need to register it in the entrypoint as the appropriate class for handling the UI of your characters.

insert  character-sheetentrypoint.mjs    30:39 js+foundry utf-8   unix  
30 import MyTestActorSheet from './character⁠-⁠sheet​.mjs'; 31 32 Hooks.on("init", () => { 33 CONFIG.Actor.dataModels.character = CharacterData; 34 foundry.documents.collections.Actors.registerSheet( 35 "my⁠-⁠test⁠-⁠system", 36 MyTestActorSheet, 37 { types: ["character"], makeDefault: true, } 38 ); 39 });

Now the MyTestActorSheet class will be used as the default for displaying any Actor of type “character,” so what remains is to write the HTML for the window and have the class declare what file to load the HTML from.

insert  character-sheetcharacter-sheet.mjs    4:10 js+foundry utf-8   unix  
class MyTestActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { static PARTS = { main: { template: "systems/my⁠-⁠test⁠-⁠system/templates/actor⁠-⁠character⁠-⁠sheet​.hbs" }, } }

It’s worth noting that any paths to resources within our system must start with systems/${SYSTEM_ID}. We’ll probably want to set up some simple helper functions to cut down on repetition, but for now let’s look at a basic template document.

normal  character-sheetactor-character-sheet.hbs    1:12 html+handlebars utf-8  
<div class="actor⁠-⁠{{document.type}}⁠-⁠sheet"> {{! Display the standard actor icon. }} <img src="{{document.img}}" class="profile⁠-⁠img" height="100" width="100" alt="Profile image for {{document.name}}" > {{! Show character name }} <p>Name: {{ document.name }} </div> {{! Log the full sheet context to the console for debug purposes. }} {{ log "Actor sheet" this level="warn" }}

This displays a simple, read-only view of the character’s name and image. Including a call to the log helper makes it easy to peruse through the browser console exactly what information is included with the context. Here I’m logging at the “warn” level because it’s the only log level Foundry doesn’t spew garbage into by default, making it easy to filter for the data I’m interested in.

In order to modify any of it, we need to create some simple form elements and a way to save changes. Foundry provides some built-in tools for that. In order to make use of them, I add a footer part using one of the built-in templates. I add the standard-form content class to the window so that the footer elements get styled appropriately.

The footer template checks the context for the presence of a buttons element, using that to dynamically create the save button. In order to add the buttons, I need to override the _prepareContext method and add the magic invocation to generate a submit button for the form.

insert  character-sheetcharacter-sheet.mjs    4:26 js+foundry utf-8   unix  
class MyTestActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { static DEFAULT_OPTIONS = { window: { contentClasses: ["standard⁠-⁠form"] }, } static PARTS = { main: { template: "systems/my⁠-⁠test⁠-⁠system/templates/actor⁠-⁠character⁠-⁠sheet​.hbs" }, footer: { template: "templates/generic/form⁠-⁠footer​.hbs", }, } async _prepareContext(options) { const context = await super._prepareContext(options); context.buttons = [ { type: "submit", icon: "fa⁠-⁠solid fa⁠-⁠save", label: "SETTINGS​.Save" }, ] return context; } }

Now I can update the handlebars template to make the image editable and add a form field for changing the name.

insert  character-sheetactor-character-sheet.hbs    1:16 html+handlebars utf-8  
<div class="actor⁠-⁠{{document.type}}⁠-⁠sheet"> {{! Display the standard actor icon. }} <img src="{{document.img}}" class="profile⁠-⁠img" {{#if editable}} data⁠-⁠action="editImage" data⁠-⁠edit="img" {{/if}} height="100" width="100" alt="Profile image for {{document.name}}" > {{! Update Character name }} {{formGroup fields.name value=document.name rootId=rootId}} </div> {{! Log the full sheet context to the console for debug purposes. }} {{ log "Actor sheet" this level="warn" }}

The formGroup helper generates a label+input pair. The first argument is a field definition. For the built-in fields such as name, these are provided by the base implementation of _prepareContext through fields. I also provide the current value of the name field as the initial value, as is standard procedure. Supplying the rootId is optional, but by doing so, formGroup gives the input an ID that comes from concatenating the rootId and the dotted field name. It also ensures the label is correctly associated with the input for assistive technologies, so it’s useful to supply it. The rootId itself is also automatically supplied, unique to the combination of actor and sheet being viewed.

Of course, now that we have the hook to supply arbitrary data to the template context, we can throw everything we want into it. Let’s add the rest.

insert  character-sheetcharacter-sheet.mjs    1:41 js+foundry utf-8   unix  
const { ActorSheetV2 } = foundry.applications.sheets; const { HandlebarsApplicationMixin } = foundry.applications.api; const { TextEditor } = foundry.applications.ux; class MyTestActorSheet extends HandlebarsApplicationMixin(ActorSheetV2) { static DEFAULT_OPTIONS = { window: { contentClasses: ["standard⁠-⁠form"] }, } static PARTS = { main: { template: "systems/my⁠-⁠test⁠-⁠system/templates/actor⁠-⁠character⁠-⁠sheet​.hbs" }, footer: { template: "templates/generic/form⁠-⁠footer​.hbs", }, } async _prepareContext(options) { const context = await super._prepareContext(options); context.system = this.actor.system; context.systemID = "my⁠-⁠test⁠-⁠system"; context.descriptionHTML = await TextEditor.enrichHTML( this.actor.system.description, { // Only show secret blocks to owner secrets: this.actor.isOwner, // For Actors and Items, enable rolls to use actor data. rollData: this.actor.getRollData(), } ); context.buttons = [ { type: "submit", icon: "fa⁠-⁠solid fa⁠-⁠save", label: "SETTINGS​.Save" }, ] return context; } } export default MyTestActorSheet;

Adding this.actor.system to the context allows the template to make use of all the fields in the character data model. I add the system ID in order to make it easy to add it as a CSS class, ensuring I can namespace all CSS selectors (though I will not be including any CSS in this post) and avoid clashing with anything else.

The most interesting addition to the context preparation is “enriching” the HTML for use in the description field. Exactly what this does remains a bit opaque to me, but I accept that this is a necessary step to make full use of HTML styling for a given field. Similarly, a special invocation is needed in the template to show the user a rich editor.

normal  character-sheetactor-character-sheet.hbs    1:58 html+handlebars utf-8  
<div class="{{systemID}} actor⁠-⁠{{document.type}}⁠-⁠sheet"> {{! Display the standard actor icon. }} <img src="{{document.img}}" class="profile⁠-⁠img" {{#if editable}} data⁠-⁠action="editImage" data⁠-⁠edit="img" {{/if}} height="100" width="100" alt="Profile image for {{document.name}}" > {{! Update Character name }} {{formGroup fields.name value=document.name rootId=rootId }} {{! Read⁠-⁠only dead status using the dead getter }} <div class="form⁠-⁠group"> <label>Dead yet?</label> <span class="dead⁠-⁠status">{{ifThen system.dead "Yes" "No"}}</span> </div> {{! Current Menace level }} {{formGroup system.schema.fields.toxicity.fields.value value=system.toxicity.value rootId=rootId }} {{! Max Menace level }} {{formGroup system.schema.fields.toxicity.fields.max label="Max" value=system.toxicity.max rootId=rootId }} {{! HTML⁠-⁠based stylable description field }} <div class="form⁠-⁠group"> <label for="{{rootId}}⁠-⁠system​.description"> {{system.schema.fields.description.label}} </label> {{#if editable}} {{! Create the rich HTML editor widget. }} <prose⁠-⁠mirror id="{{rootId}}⁠-⁠system​.description" name="system​.description" button="true" editable="{{editable}}" toggled="false" value="{{system.description}}" > {{{descriptionHTML}}} </prose⁠-⁠mirror> {{else}} {{! Create a plain HTML div with the content }} <div class="uneditable⁠-⁠content" id="{{rootId}}⁠-⁠system​.description"> {{{descriptionHTML}}} </div> {{/if}} </div> </div> {{! Log the full sheet context to the console for debug purposes. }} {{ log "Actor sheet" this level="warn" }}

The above template adds two formGroup instances to modify the current and max toxicity level. It also uses the dead getter on the data model to identify whether the character should be dead based on those values.

More complex is the description field. The formGroup helper does not handle creating an editor, so I have written out the HTML for it myself. Using the form-group class on the outer div ensures the label is styled the same way as the others. I have also supplied a value for the input’s id that is equivalent to what the formGroup helper would generate.

At the time of this writing, the prose-mirror tag is entirely undocumented, though I am told that as of Foundry v13, it is the current best practice for creating a rich editor. (Version 14 was just recently released, adding no new documentation for it.) I am told this element is flexible in its functionality, but one would have to dig into the source code to tease out what secrets it holds.

In any case, here we have a simple, editable character sheet with a few fields.

Next Steps

Of course, a real character sheet would have many more fields than this, and also it would need proper CSS styling to make it easy and comfortable to navigate. The system.json file provides a styles field for this purpose. Additionally, splitting the template into additional parts will enable me to make use of Foundry’s support for a tabbed UI.

And naturally we’ll want to hook into the dice rolling system as well, making it easy for users to roll whatever checks this system demands. Abilities, items, and drag/drop also need further development. As I learn how to make use of all these features, I hope to document them here in case someone else out there might make use of this. Certainly, this blog post contains what I hoped would be documented before I started this project.