| name | foundryvtt-system |
| description | This skill should be used when the user asks about "FoundryVTT system development", "FoundryVTT module", "Foundry hooks", "data model", "template.json", "Actor class", "Item class", "Roll class", "compendium packs", "game settings", "game.i18n", "localize", "i18n", "testing FoundryVTT", "Vitest with Foundry", "mocking Foundry", "system.json", "FoundryVTT project structure", "Foundry documents", "embedded documents", "settings registration", "Hooks.on", "Hooks.once", "game.settings.register", "game.actors", "game.items", "CONFIG object", "FoundryVTT canvas", "FoundryVTT API", "SCSS in Foundry", "language files", "translation files", "pack management", "LevelDB packs", "compendium JSON", or mentions building a game system or module for FoundryVTT. |
| version | 1.0.0 |
FoundryVTT System Development Guide
This guide covers the fundamentals of building a game system or module for FoundryVTT v13. It provides patterns for project structure, data models, hooks, documents, settings, testing, and internationalization.
Note: This plugin is a community resource and is not affiliated with or endorsed by Foundry Gaming LLC.
System File Structure
A typical FoundryVTT system:
my-system/
+-- module/ # JavaScript source
| +-- __tests__/ # Test files (Vitest)
| +-- __mocks__/ # Test mocks for Foundry API
| +-- my-system.js # Entry point (registered in system.json)
| +-- actor.js # Custom Actor class
| +-- item.js # Custom Item class
| +-- config.js # System constants and lookup tables
| +-- ...
+-- templates/ # Handlebars templates (.html)
+-- styles/ # SCSS/CSS stylesheets
+-- lang/ # Translation files (en.json, de.json, etc.)
+-- packs/ # Compendium packs
| +-- creatures/
| | +-- src/ # JSON source files (tracked in git)
| +-- items/
| +-- src/
+-- docs/ # Documentation
+-- system.json # System manifest
+-- template.json # Data model definition
Data Model (template.json)
Defines the structure of Actor and Item data:
{
"Actor": {
"types": ["character", "npc"],
"character": {
"hp": { "value": 10, "max": 10 },
"level": 1,
"abilities": {
"str": { "value": 10 },
"dex": { "value": 10 },
"con": { "value": 10 },
"int": { "value": 10 },
"wis": { "value": 10 },
"cha": { "value": 10 }
},
"biography": ""
},
"npc": {
"hp": { "value": 10, "max": 10 },
"cr": 1,
"description": ""
}
},
"Item": {
"types": ["weapon", "armor", "spell", "equipment"],
"weapon": {
"damage": "1d6",
"attackBonus": 0,
"equipped": false
},
"armor": {
"ac": 10,
"equipped": false
},
"spell": {
"level": 1,
"description": ""
},
"equipment": {
"quantity": 1,
"weight": 0,
"description": ""
}
}
}
Access in code: actor.system.hp.value, item.system.damage
Document Types
Actor
class MyActor extends Actor {
prepareData() {
super.prepareData()
const system = this.system
for (const [key, ability] of Object.entries(system.abilities)) {
ability.mod = Math.floor((ability.value - 10) / 2)
}
}
async rollAbilityCheck(abilityId) {
const ability = this.system.abilities[abilityId]
const roll = new Roll('1d20 + @mod', { mod: ability.mod })
await roll.evaluate()
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor: this }),
flavor: game.i18n.localize(`MY_SYSTEM.Ability${abilityId.capitalize()}`)
})
return roll
}
}
Item
class MyItem extends Item {
prepareData() {
super.prepareData()
}
async roll() {
const speaker = ChatMessage.getSpeaker({ actor: this.actor })
await ChatMessage.create({
speaker,
content: `<h3>${this.name}</h3><p>${this.system.description}</p>`
})
}
}
Hooks System
Lifecycle Hooks
Hooks.once('init', () => {
CONFIG.Actor.documentClass = MyActor
CONFIG.Item.documentClass = MyItem
})
Hooks.once('ready', () => {
})
Common Hooks
| Hook | When | Parameters |
|---|
init | System init, before game data | None |
ready | Game fully loaded | None |
createActor | Actor created | (actor, options, userId) |
updateActor | Actor updated | (actor, change, options, userId) |
deleteActor | Actor deleted | (actor, options, userId) |
createItem | Item created | (item, options, userId) |
updateItem | Item updated | (item, change, options, userId) |
deleteItem | Item deleted | (item, options, userId) |
renderChatMessageHTML | Chat message rendered (V13) | (message, html, data) |
preCreateChatMessage | Before chat message created | (message, data, options, userId) |
hotbarDrop | Macro dropped on hotbar | (bar, data, slot) |
getChatMessageContextOptions | Context menu on chat (V13) | (application, menuItems) |
Hook Patterns
Hooks.on('updateActor', (actor, change, options, userId) => {
if (userId !== game.user.id) return
if (change.system?.hp) {
console.log(`${actor.name} HP changed`)
}
})
Hooks.on('getChatMessageContextOptions', (application, menuItems) => {
menuItems.push({
name: 'MY_SYSTEM.ApplyDamage',
icon: '<i class="fas fa-heart-broken"></i>',
condition: element => {
const messageId = element.dataset.messageId
return game.messages.get(messageId)?.isRoll
},
callback: element => {
const messageId = element.dataset.messageId
const message = game.messages.get(messageId)
}
})
})
Settings Registration
Hooks.once('init', () => {
game.settings.register('my-system', 'criticalHitRule', {
name: 'MY_SYSTEM.Settings.CriticalHitRule',
hint: 'MY_SYSTEM.Settings.CriticalHitRuleHint',
scope: 'world',
config: true,
type: String,
choices: {
double: 'MY_SYSTEM.Settings.DoubleDamage',
maxPlusRoll: 'MY_SYSTEM.Settings.MaxPlusRoll'
},
default: 'double'
})
game.settings.register('my-system', 'showDamageOverlay', {
name: 'MY_SYSTEM.Settings.ShowDamageOverlay',
scope: 'client',
config: true,
type: Boolean,
default: true
})
game.settings.register('my-system', 'schemaVersion', {
scope: 'world',
config: false,
type: Number,
default: 0
})
})
const rule = game.settings.get('my-system', 'criticalHitRule')
await game.settings.set('my-system', 'schemaVersion', 2)
Roll System
const roll = new Roll('1d20 + @mod', { mod: 5 })
await roll.evaluate()
await roll.toMessage({ speaker: ChatMessage.getSpeaker({ actor }) })
const roll = new Roll('2d6 + @bonus', { bonus: actor.system.attackBonus })
await roll.evaluate()
await roll.toMessage({
speaker: ChatMessage.getSpeaker({ actor }),
flavor: game.i18n.localize('MY_SYSTEM.AttackRoll')
})
await ChatMessage.create({
content: `<p>Damage: [[/r 1d8 + 3]]</p>`,
speaker: ChatMessage.getSpeaker({ actor })
})
Compendium / Pack Management
FoundryVTT uses LevelDB for compendium storage. Maintain JSON source files in git:
packs/
+-- creatures/
| +-- src/
| +-- goblin.json
| +-- dragon.json
+-- items/
+-- src/
+-- longsword.json
Workflow:
- Edit JSON source files in
packs/*/src/
- Compile to LevelDB:
fvtt package pack <pack-name>
- Test in FoundryVTT
- Commit JSON changes to git
Git tracking:
- DO commit:
packs/*/src/*.json
- DO NOT commit: LevelDB files (
.db)
CSS/SCSS Patterns
Edit SCSS source files, compile to CSS:
.my-system {
color: var(--system-text-color);
background: var(--system-background-color);
.sheet-header {
display: flex;
align-items: center;
gap: 0.5rem;
.profile-img {
width: 100px;
height: 100px;
object-fit: cover;
border: 2px solid var(--system-border-color);
}
}
.item-list {
list-style: none;
padding: 0;
.item {
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem;
border-bottom: 1px solid var(--system-border-color);
}
}
}
Internationalization (i18n)
All user-facing text must use the i18n system:
const label = game.i18n.localize('MY_SYSTEM.AbilityStr')
const message = game.i18n.format('MY_SYSTEM.RollResult', {
actor: actorName,
result: rollTotal
})
{{!-- In templates --}}
<label>{{localize "MY_SYSTEM.AbilityStr"}}</label>
Language files (lang/en.json):
{
"MY_SYSTEM": {
"AbilityStr": "Strength",
"AbilityDex": "Dexterity",
"RollResult": "{actor} rolled {result}",
"Settings": {
"CriticalHitRule": "Critical Hit Rule"
}
}
}
Testing Overview
Use Vitest with mocks for the FoundryVTT API:
import { vi, describe, it, expect, beforeEach } from 'vitest'
import '../__mocks__/foundry.js'
describe('MyActor', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('should calculate ability modifiers', () => {
const actor = createTestActor({ abilities: { str: { value: 16 } } })
actor.prepareData()
expect(actor.system.abilities.str.mod).toBe(3)
})
})
See references/testing-and-i18n.md for comprehensive testing and i18n patterns.
Additional Resources
Reference Files
references/hooks-and-documents.md — Complete hooks lifecycle, document class hierarchy, embedded documents, canvas system, game object patterns, settings API, socket communication, Roll class
references/testing-and-i18n.md — Vitest setup, mocking Foundry APIs, test patterns, coverage goals, language file structure, localization best practices, pack management workflow