Declarative sheet bases
BaseActorSheet and BaseItemSheet handle drag-drop wiring, tab state, and fromUuid resolution. What is left to write is the rules of your game.
An SDK for building FoundryVTT v14+ systems and modules. Typed data models, sheets that already know their tabs and drops, and a dev loop that reloads in place.
What breaks is the sheet plumbing, the registration boilerplate, the class that moved namespace. VTTForge holds that in one place, so when Foundry moves it, you update a dependency instead of rewriting a sheet. If this project ever goes quiet, the output is plain ES modules Foundry loads natively: delete it and your code is still your code.
class CharacterSheet extends ActorSheetV2 { // 36 lines of identical drag-drop wiring #createDragDropHandlers() { return this.options.dragDrop.map(d => { d.callbacks = { dragstart: this._onDragStart.bind(this), drop: this._onDrop.bind(this), }; return new DragDrop(d); }); } async _prepareContext(options) { const context = await super._prepareContext(options); context.tabs = { primary: this._prepareTabs("primary"), /* every group, every sheet… */ }; return context; } async _onDropItem(event, data) { const item = await fromUuid(data.uuid); if (!item) return; // type-switching, validation, transfer… } }
import { BaseActorSheet } from "@vttforge/core"; class CharacterSheet extends BaseActorSheet() { static PARTS = { main: { template: "systems/demo/sheet.hbs" }, }; static TABS = { primary: { tabs: ["abilities", "inventory", "bio"] }, }; static DRAG_DROP = [{ dragSelector: ".item" }]; // fromUuid already resolved, just handle your domain. async onDropItem(item, event) { await this.actor.createEmbeddedDocuments("Item", [item.toObject()]); } }
A published Foundry module runs every API before it lands in the SDK. Where Foundry already does something well, the SDK leaves it alone.
BaseActorSheet and BaseItemSheet handle drag-drop wiring, tab state, and fromUuid resolution. What is left to write is the rules of your game.
One defineSchema() drives both runtime validation and your TypeScript types via InferSchema<T>. There is no second declaration to keep in sync.
@vttforge/styles nests under Foundry's system layer with a vendored prefix, so your rules win without !important, and it inherits Theme V2.
createMigrationRunner() runs versioned migrations once at world load, GM only. A failure stops at the last version that finished, so the next load retries from there.
Save a template and only the windows using it redraw. Save a stylesheet and the CSS swaps. No page reload, no lost scroll position.
Every runtime error carries a VTTF-NNNN code from an append-only registry. Codes never change across majors, so a log line from last year still resolves to a docs page.
Install what you need. @vttforge/core is enough for a system or a module; the rest layer on top. All on npm, with provenance.
| package | what it does | status |
|---|---|---|
| @vttforge/core | Runtime: registerSystem / registerModule, sheet and enricher registration, BaseTypeDataModel with inference, BaseActorSheet / BaseItemSheet / BaseDocumentSheet / BaseApplication, PackageConfig, createMigrationRunner, VttfError. For modules: registerSocket, moduleApi, convertSubTypes, inject | on npm |
| @vttforge/cli | vttforge init / dev / build / audit / migrate / lint: four templates, the symlink-and-watch dev loop, the release zip, the v14 audit, the v13-to-v14 codemod and a bundled linter | on npm |
| create-vttforge | bun create vttforge, the scaffolder as a create package | on npm |
| @vttforge/vite-plugin | The build contract: browser ESM, no hashing, class names kept, CSS bundled, manifest copied with version synced | on npm |
| @vttforge/styles | The Forge design system: DTCG tokens compiled to CSS, sheet primitives, opt-in themes over cascade layers | on npm |
| @vttforge/testing | withMockFoundry for Vitest, which records sheets and enrichers, plus a Quench half for a real world | on npm |
| @vttforge/dev-module | The in-world half of the dev loop; vttforge dev installs it | on npm |
| @vttforge/types | The Foundry surface the bases stand on: ApplicationV2Members, DocumentSheetV2Members, VttforgeClass. Core re-exports them | on npm |
The repo ships a real Foundry v14 system and a module you can boot in Docker: tabbed character sheet, drag-drop inventory, demo migration, and a module that talks over a socket and adds a button to the sidebar. Requires Docker + a foundryvtt.com license.
Clone the repo, fill in your Foundry license, run one compose command. The bundled examples/simple-system mounts read-only at localhost:30000.
# clone & install git clone https://github.com/vttforge/vttforge cd vttforge bun install bun run build # fill in your foundryvtt.com license cp .env.example .env # boot Foundry v14 with the example system mounted docker compose -f docker-compose.dev.yml up # http://localhost:30000
One command writes a system or a module you can open in Foundry: typed data models, a sheet with tabs and drag-drop, a migration runner, a release workflow. Then vttforge dev reloads templates and styles in place. The walkthrough is in getting started.
# a system, or a module bun create vttforge my-system \ --type system --lang ts cd my-system # link into Foundry, watch, reload in place vttforge dev # lint the manifest and source for v14 breakages vttforge audit # dist/ plus the release zip vttforge build
Every package is on npm. Scaffold one, read the guide, or file an issue with the boilerplate you keep writing by hand.