ThirdEra

Compendium Building Guide

This document captures learnings and best practices for creating and managing Foundry VTT compendiums in the ThirdEra system.

Overview

Compendiums in Foundry VTT are collections of pre-made game content (items, actors, etc.) that can be imported into worlds. ThirdEra uses compendiums to distribute SRD content (races, classes, skills, feats, spells, weapons, armor, equipment) so users don't have to manually create every item.

Compendium Architecture

Storage Model

Critical Understanding: Foundry persists compendium content in database files under each packs/<name>/ directory. ThirdEra also keeps JSON source files in those folders; on world load the system’s CompendiumLoader (GM) reads that JSON and creates or updates matching compendium documents (including races).

Races pack (JSON refresh)

On each world load (GM client, ready), CompendiumLoader refreshes all mapped packs from packs/*.json, including Races: existing compendium race documents are updated from disk when the stable key (race name) matches, so compendium entries track system releases. Edits you make inside the Races compendium are overwritten on the next load—use world race Items (or another pack you control) for homebrew or experiments you need to keep. New race JSON files are still created when missing from the pack.

SRD race mechanical rows (GMS): On ready, the GM client also runs a one-way merge (module/logic/race-srd-changes-merge.mjs) that appends missing bundled skill/save/hide system.changes rows to existing race items (Races compendium, world race items, and races embedded on actors) when flags.thirdera.raceStockDeltaRev is below the current revision (RACE_STOCK_DELTA_REV in that module). It does not remove or replace documents, does not add a row if that modifier key already appears in changes (so custom values for the same key are left alone), and does not touch ability-score rows. Self-heal: If the revision flag is already current but a race that belongs to the Races compendium (or is embedded with a thirdera_races compendium sourceId/UUID) is still missing bundled rows for its stock name, the same pass merges those rows in again — so incomplete compendium entries (e.g. Elf with only ability adjustments) are repaired without bumping RACE_STOCK_DELTA_REV. Homebrew races in the world that happen to share a stock name are not altered. When you add new stock delta rows for a release, bump RACE_STOCK_DELTA_REV. Regression tests: test/unit/data/race-pack-stock-changes.test.mjs asserts each race JSON still includes every row returned by getRaceStockDeltaRowsForName — run npm test after editing race packs or sync scripts.

Other racial traits (HTML): Stock races ship system.otherRacialTraits for vision, immunities, weapon familiarity, languages, spell-like abilities, and similar traits that are not represented as numeric system.changes rows. The field is edited on the race sheet (Details) and shown on the PC sheet Description tab. On ready, the GM client runs module/logic/race-qualitative-traits-stock.mjs: when flags.thirdera.raceQualitativeTraitsRev is below the bundled revision, empty fields are filled from stock text keyed by default race names, and known obsolete bundled wording may be replaced (see isStaleBundledQualitativeTraitsHtml in that module). Custom text is otherwise left as-is while the flag is advanced. Keep the stock map aligned with packs/races/*.json when editing shipped prose.

Compendium Pack Definition (system.json)

Each compendium pack must be declared in system.json under the "packs" array:

{
  "name": "thirdera_races",
  "label": "Races",
  "path": "packs/races",
  "type": "Item",
  "system": "thirdera",
  "ownership": {
    "GAMEMASTER": "OWNER",
    "ASSISTANT": "OWNER",
    "TRUSTED": "OBSERVER",
    "PLAYER": "OBSERVER"
  }
}

Key Fields: - name: Must use underscores, not dots (e.g., thirdera_races not thirdera.races) for Foundry V14 compatibility - label: Display name shown in the Compendium sidebar - path: Relative path to the directory containing JSON source files - type: Document type — "Item" for races, classes, spells, etc.; "Actor" for the Monsters (SRD) pack (thirdera_monsters, NPC actors). - system: System ID ("thirdera") - ownership: REQUIRED - Without this, compendiums won't be visible in the UI. Defines permissions for each user role. - banner: Optional. A file path to a banner image shown behind each compendium entry in the Compendium sidebar (and in the compendium window header when opened). If omitted, Foundry uses the default for the pack type (e.g. the same generic Item banner for all Item packs).

Compendium banner images (per-pack)

You can give each pack its own banner by adding a "banner" property to that pack in system.json:

{
  "name": "thirdera_armor",
  "label": "Armor",
  "path": "packs/armor",
  "type": "Item",
  "system": "thirdera",
  "banner": "systems/thirdera/assets/banners/armor-banner.webp",
  "ownership": { ... }
}

Image characteristics:

Spec Value
Recommended dimensions 290 × 70 pixels (Foundry's documented size for the sidebar strip).
Aspect Landscape; the image is displayed with object-fit: cover and object-position: center, so it will be cropped to fit. A 290×70 (or proportional) landscape works best.
Where to put files Inside the system, e.g. assets/banners/ (path in system.json: systems/thirdera/assets/banners/your-banner.webp).
Formats WebP, PNG, or JPEG. Foundry core uses .webp.
Display The image is shown at ~80% opacity with a slight dark overlay; the pack name and icon are centered on top. Design so important elements remain visible with that overlay.
Contexts Same image is used in the sidebar list (70px tall) and in the compendium window header when a pack is opened (100px tall).

Collection IDs

Foundry VTT uses collection IDs to reference compendium packs. The format is: {systemId}.{packName}

Example: For pack name thirdera_races, the collection ID is thirdera.thirdera_races

Important: When looking up packs in code, use the collection ID:

const pack = game.packs.get("thirdera.thirdera_races");

Compendium Loader (module/logic/compendium-loader.mjs)

The CompendiumLoader class programmatically imports JSON files into compendiums.

Initialization

Called from thirdera.mjs on the Hooks.once("ready") hook:

Hooks.once("ready", () => {
    CompendiumLoader.init();
});

Key Methods

init()

loadPackFromJSON(pack, fileList)

FILE_MAPPINGS

Maps collection IDs to arrays of JSON filenames:

static FILE_MAPPINGS = {
    "thirdera.thirdera_races": [
        "race-dwarf.json",
        "race-elf.json",
        // ...
    ],
    // ...
};

Important: Keys must match Foundry's collection IDs (systemId.packName).

Monster (NPC) compendium (thirdera_monsters)

JSON File Structure

Required Fields

All compendium JSON files must follow this structure:

{
  "_id": "item-id",
  "name": "Item Name",
  "type": "itemType",
  "img": "icons/svg/icon.svg",
  "system": {
    // Type-specific data matching the TypeDataModel schema
  },
  "flags": {},
  "folder": null,
  "sort": 0
}

ID Handling

Critical: Foundry VTT requires _id values to be exactly 16 alphanumeric characters. JSON files with descriptive IDs (like "race-dwarf") will cause validation errors.

Solution: The loader removes invalid _id values before importing, allowing Foundry to generate valid IDs automatically:

if (jsonData._id && !jsonData._id.match(/^[a-zA-Z0-9]{16}$/)) {
    delete jsonData._id;
}

Icon Paths

Icons must use paths that exist in Foundry VTT's icon set. Common paths: - icons/svg/sword.svg - Weapons, combat feats - icons/svg/shield.svg - Armor, defensive feats - icons/svg/target.svg - Ranged weapons, archery feats - icons/svg/aura.svg - Magic, spells, metamagic feats - icons/svg/book.svg - Knowledge, skills, item creation feats - icons/svg/temple.svg - Holy symbols, religious items - icons/svg/pawprint.svg - Animals, mounted combat - icons/svg/eye.svg - Perception, awareness - icons/svg/invisible.svg - Stealth, hidden things

Icon Validation: Verify icons exist in your Foundry installation's public icon set (e.g. resources/app/public/icons/svg/ under the Foundry app directory) before using them.

Spell Compendium Format

Spell compendium JSON should use the current schema (not legacy fields) so content does not depend on migration.

Icon strategy: use a single default (e.g. icons/svg/aura.svg) or a small set by school; verify paths in Foundry's icon set.

Domain Compendium Format

Domain items have system.description and system.key only. Domain spell lists are not stored on the domain; they are derived at runtime from spell items' levelsByDomain (world items + spells compendium). Domain compendium JSONs must not include a spells array.

Compendium Locking

Compendiums are locked by default in Foundry VTT. You must unlock them before creating or updating documents:

if (pack.locked) {
    await pack.configure({ locked: false });
}

Updating Existing Content

The loader supports updating existing compendium entries:

  1. Fetches existing documents from the compendium
  2. Matches by stable key (not name alone): system.conditionId for conditions, otherwise system.key if set, otherwise name. So you can change a document's name in JSON (e.g. to "Evasion (Rogue)") and the loader will update the same compendium document as long as system.key is unchanged.
  3. Updates existing items or creates new ones
  4. Uses Document.updateDocuments() for updates, Document.createDocuments() for new items

This allows updating compendium content (e.g., fixing icons or disambiguating display names) without creating duplicates.

Key convention for new content

New document keys should use UUIDs (or UUID-like unique identifiers). Human-readable keys that are similar across documents (e.g. evasion in multiple classes) can cause confusion; UUIDs keep each document's key globally distinct and avoid collisions when names are alike. Existing keys remain as-is; this convention applies to new compendium entries and should be followed when adding content.

Class Features compendium: Feature document names in the compendium may include "(ClassName)" for disambiguation (e.g. "Evasion (Rogue)" vs "Evasion (Monk)"). Character sheet, class sheet, and level-up flow show the short name (e.g. "Evasion") from the class data (featName), not the compendium document name.

Common Issues and Solutions

Compendiums Not Visible

Problem: Compendiums don't appear in the Compendium sidebar.

Solution: Add ownership field to pack definition in system.json. Without it, Foundry won't display the compendium.

"Pack not found" Errors

Problem: game.packs.get(packName) returns undefined.

Solution: Use the collection ID format: thirdera.thirdera_races not just thirdera_races.

Path Duplication (404 Errors)

Problem: File paths like systems/thirdera/systems/thirdera/packs/... cause 404 errors.

Solution: Normalize paths by removing existing systems/thirdera/ prefix:

let normalizedPath = pack.metadata.path;
if (normalizedPath.startsWith('systems/thirdera/')) {
    normalizedPath = normalizedPath.replace(/^systems\/thirdera\//, '');
}
const basePath = `systems/thirdera/${normalizedPath}`;

Invalid ID Validation Errors

Problem: DataModelValidationError: _id: must be a valid 16-character alphanumeric ID

Solution: Remove invalid _id values from JSON before importing. Foundry will generate valid IDs automatically.

"importDocuments is not a function"

Problem: Trying to use pack.importDocuments().

Solution: Use Document.createDocuments() with pack option:

await DocumentClass.implementation.createDocuments(documents, {pack: pack.collection});

"locked compendium" Errors

Problem: Cannot create documents in locked compendium.

Solution: Unlock the compendium before creating documents:

if (pack.locked) {
    await pack.configure({ locked: false });
}

Icon or content updates not appearing after restart

Problem: You changed JSON (e.g. img paths) and restarted Foundry, but the compendium still shows old icons or content.

Cause: Foundry stores compendium data in LevelDB files (.ldb, CURRENT, LOG, MANIFEST-*, etc.) inside each pack directory. The loader updates existing documents by name; if the server or index keeps serving cached data, or updates don't persist as expected, the pack continues to show old values.

Solution: Clear the pack's LevelDB cache so the loader re-imports from JSON (it will create all documents fresh). Do this with Foundry fully stopped.

  1. Stop Foundry (close the app or stop the headless process).
  2. From the repo root, remove only the LevelDB files in the pack directory (do not delete the .json source files):

bash # Class Features pack rm -f packs/features/*.ldb packs/features/CURRENT packs/features/LOCK packs/features/LOG packs/features/LOG.old packs/features/MANIFEST-*

  1. Start Foundry and load the world. The loader will see an empty pack and run createDocuments() for all JSON files, so the new img and content will appear.

To clear another pack (e.g. packs/races), use the same pattern: delete *.ldb, CURRENT, LOCK, LOG, LOG.old, MANIFEST-* in that directory only.

Best Practices

  1. Always include ownership in pack definitions
  2. Use underscores in pack names (not dots) for V14 compatibility
  3. Remove invalid _id values from JSON before importing
  4. Unlock compendiums before creating/updating documents
  5. Use collection IDs (systemId.packName) when looking up packs
  6. Normalize file paths to avoid duplication
  7. Update existing items by name to support content updates
  8. Verify icon paths exist in Foundry's icon set
  9. Match JSON structure to the TypeDataModel schema exactly
  10. Test after changes - restart Foundry and verify compendiums populate correctly

Current Compendium Status

Complete

Incomplete

Capability grants (CGS) in pack JSON

Structured grants belong in system.cgsGrants (and spell target restrictions on system.targetCreatureType*), not in numeric system.changes. See Development — Capability grants for categories and merge behavior.

Stable keys vs compendium UUIDs

Foundry assigns document UUIDs when compendium items are created. Pack source JSON under packs/ usually does not include those UUIDs. Authors therefore use stable keys that match system.key on Creature Type, Subtype, and Spell items; the GM ready hook runs CompendiumLoader.resolveCgsReferenceKeysInPacks() (after JSON import) and writes resolved UUIDs into the live compendium documents.

Location Authoring fields Resolved to
Spells system.key (slug, e.g. holdPerson — use node scripts/add-spell-keys-from-filename.mjs after adding spells) Used to resolve spellGrant.spellKey in other items
Spells system.targetCreatureTypeKeys, system.targetCreatureSubtypeKeys (match system.key on type/subtype items) system.targetCreatureTypeUuids (types and subtypes share one list for targeting)
Armor, weapons, equipment system.mechanicalCreatureGateTypeKeys, system.mechanicalCreatureGateSubtypeKeys system.mechanicalCreatureGateUuids
cgsGrants.grants on feats, features, races, conditions, gear spellKey on { "category": "spellGrant", … }; typeKey / subtypeKey on overlay grants spellUuid, typeUuid, subtypeUuid

Git remains the source of truth: keep keys in JSON; compendium copies gain UUIDs at runtime. Re-import pack JSON (system setting Re-import compendium JSON on each load) to refresh from disk, then resolution runs again.

Feats and class features (Phase 2 pack authoring)

Gear (Phase 3 — armor, weapons, equipment)

Helper scripts (repo root)

Adding New Compendium Content

  1. Create JSON file in appropriate packs/ subdirectory
  2. Match the TypeDataModel schema exactly
  3. Use valid Foundry icon paths
  4. Remove or omit _id (or use valid 16-char alphanumeric)
  5. Add filename to FILE_MAPPINGS in compendium-loader.mjs
  6. Restart Foundry - loader will automatically import/update the content
🡐 Development