📚 Tome Integration Guide

Using .tome files in your applications

What is Tome?

Tome (Tabletop Open Markup for Entities) is a universal, system-agnostic format for representing RPG entities. Whether you're building a character sheet, a creature database, or an item catalog, Tome provides a flexible structure that works across any game system.

Key Benefit: Users can create entities once and use them across multiple applications - from character editors to virtual tabletops to campaign management tools.

Installation

NPM / Yarn

npm install tome
# or
yarn add tome

Browser (CDN)

<script type="module">
  import { TomeEntity } from 'https://unpkg.com/tome/dist/index.js';
</script>

Basic Usage

Loading a .tome File

import { TomeEntity } from 'tome';

// From a file upload
async function loadTomeFile(file) {
  const text = await file.text();
  const entity = TomeEntity.fromJSON(text);

  console.log(entity.identity.name.primary); // "Aria Swiftblade"
  console.log(entity.meta.type); // "character"

  return entity;
}

// From a URL
async function loadTomeFromURL(url) {
  const response = await fetch(url);
  const data = await response.json();
  return TomeEntity.fromJSON(JSON.stringify(data));
}

Creating a New Entity

import { TomeEntity } from 'tome';

const character = new TomeEntity({
  type: 'character',
  name: 'Thorin Ironshield',
  tags: ['dwarf', 'fighter', 'tank']
});

// Add properties
character.setProperty('strength', 18, 'static');
character.setProperty('level', 5, 'dynamic');

// Add resources
character.setResource('health', { current: 65, maximum: 65 });

// Export as JSON
const json = character.toJSON();
const blob = new Blob([json], { type: 'application/json' });
// Save or share the .tome file

Virtual Tabletop Integration

Use Case: Roll20-style VTT

Allow players to import their characters from any character editor that exports .tome files

Import Flow

// VTT Client Code
class VTTCharacterImporter {
  constructor(gameSession) {
    this.gameSession = gameSession;
  }

  async importFromTome(tomeFile) {
    const text = await tomeFile.text();
    const entity = TomeEntity.fromJSON(text);

    // Validate it's a character
    if (entity.meta.type !== 'character') {
      throw new Error('Only character entities can be imported');
    }

    // Convert to your VTT's internal format
    const vttCharacter = this.convertToVTTFormat(entity);

    // Add to the game session
    await this.gameSession.addCharacter(vttCharacter);

    return vttCharacter;
  }

  convertToVTTFormat(tomeEntity) {
    const properties = tomeEntity.properties || {};

    return {
      id: tomeEntity.meta.id,
      name: tomeEntity.identity.name.primary,
      avatar: tomeEntity.identity.image?.portrait || null,

      // Map properties to your stat system
      stats: {
        ...properties.static,
        ...properties.dynamic
      },

      // Map resources to health/mana bars
      resources: Object.entries(tomeEntity.resources || {}).map(([key, res]) => ({
        name: key,
        current: res.current,
        max: res.maximum,
        color: this.getResourceColor(key)
      })),

      // Map capabilities to action buttons
      actions: (tomeEntity.capabilities?.actions || []).map(action => ({
        id: action.id,
        name: action.name,
        description: action.description,
        icon: action.icon || 'default'
      })),

      // Store original tome data for export
      _tomeData: tomeEntity.data
    };
  }

  getResourceColor(resourceName) {
    const colorMap = {
      health: '#e74c3c',
      mana: '#3498db',
      stamina: '#2ecc71',
      energy: '#f39c12'
    };
    return colorMap[resourceName.toLowerCase()] || '#95a5a6';
  }

  // Export back to .tome format
  async exportToTome(vttCharacter) {
    // Reconstruct from stored tome data
    const entity = TomeEntity.fromJSON(
      JSON.stringify(vttCharacter._tomeData)
    );

    // Update with current values
    entity.resources.health.current = vttCharacter.resources.find(
      r => r.name === 'health'
    )?.current;

    return entity.toJSON();
  }
}

Token/Character Sheet Display

// Display character on the VTT canvas
class CharacterToken {
  constructor(tomeEntity, position) {
    this.entity = tomeEntity;
    this.position = position;
    this.sprite = this.loadSprite();
  }

  loadSprite() {
    // Use portrait from tome
    const imageUrl = this.entity.identity.image?.portrait;
    return new PIXI.Sprite.from(imageUrl || 'default-token.png');
  }

  render(ctx) {
    // Draw token
    ctx.drawImage(this.sprite, this.position.x, this.position.y);

    // Draw health bar
    const health = this.entity.resources?.health;
    if (health) {
      const barWidth = 60;
      const percentage = health.current / health.maximum;

      ctx.fillStyle = '#333';
      ctx.fillRect(this.position.x, this.position.y - 10, barWidth, 6);

      ctx.fillStyle = percentage > 0.5 ? '#2ecc71' : '#e74c3c';
      ctx.fillRect(this.position.x, this.position.y - 10, barWidth * percentage, 6);
    }
  }

  // Handle character taking damage
  takeDamage(amount) {
    if (this.entity.resources?.health) {
      this.entity.resources.health.current = Math.max(
        0,
        this.entity.resources.health.current - amount
      );
    }
  }
}

Character Editor Integration

Use Case: System-Specific Character Builder

Create a character editor like savaged.us that exports to .tome format for portability

Export from Custom Editor

// Character Editor Code (e.g., Savage Worlds editor)
class SavageWorldsCharacter {
  constructor(characterData) {
    this.characterData = characterData;
  }

  exportToTome() {
    const entity = new TomeEntity({
      type: 'character',
      name: this.characterData.name,
      tags: ['savage-worlds', ...this.characterData.hindrances],
      system: 'Savage Worlds Adventure Edition'
    });

    // Description
    entity.identity.description = {
      short: this.characterData.concept,
      full: this.characterData.background,
      appearance: this.characterData.appearance
    };

    // Attributes (Savage Worlds: d4-d12)
    entity.setProperty('agility', this.characterData.agility, 'static');
    entity.setProperty('smarts', this.characterData.smarts, 'static');
    entity.setProperty('spirit', this.characterData.spirit, 'static');
    entity.setProperty('strength', this.characterData.strength, 'static');
    entity.setProperty('vigor', this.characterData.vigor, 'static');

    // Skills
    Object.entries(this.characterData.skills).forEach(([skill, die]) => {
      entity.setProperty(\`skill_\${skill}\`, die, 'dynamic');
    });

    // Derived stats
    entity.setProperty('pace', this.characterData.pace, 'computed');
    entity.setProperty('parry', this.calculateParry(), 'computed');
    entity.setProperty('toughness', this.calculateToughness(), 'computed');

    // Wounds and Fatigue (resources)
    entity.setResource('wounds', {
      current: 0,
      maximum: 3
    });
    entity.setResource('fatigue', {
      current: 0,
      maximum: 2
    });
    entity.setResource('bennies', {
      current: 3,
      maximum: 3
    });

    // Edges (capabilities)
    this.characterData.edges.forEach(edge => {
      entity.addCapability('passive', {
        id: edge.id,
        name: edge.name,
        description: edge.effect,
        requirements: edge.requirements
      });
    });

    // Powers (if applicable)
    this.characterData.powers?.forEach(power => {
      entity.addCapability('action', {
        id: power.id,
        name: power.name,
        description: power.effect,
        cost: \`\${power.powerPoints} PP\`,
        range: power.range,
        duration: power.duration
      });
    });

    // Add power points as resource
    if (this.characterData.powerPoints) {
      entity.setResource('power_points', {
        current: this.characterData.powerPoints.current,
        maximum: this.characterData.powerPoints.maximum
      });
    }

    // Gear
    entity.data.inventory = {
      items: this.characterData.gear.map(item => ({
        id: item.id,
        name: item.name,
        quantity: item.quantity,
        weight: item.weight,
        notes: item.notes
      }))
    };

    return entity.toJSON();
  }

  calculateParry() {
    const fighting = this.characterData.skills.fighting || 'd4';
    const dieValue = parseInt(fighting.substring(1));
    return 2 + Math.floor(dieValue / 2);
  }

  calculateToughness() {
    const vigor = parseInt(this.characterData.vigor.substring(1));
    const armor = this.characterData.armor || 0;
    return 2 + Math.floor(vigor / 2) + armor;
  }
}

// Export button handler
document.getElementById('exportTome').addEventListener('click', () => {
  const character = new SavageWorldsCharacter(getCurrentCharacter());
  const tomeJSON = character.exportToTome();

  // Download .tome file
  const blob = new Blob([tomeJSON], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const link = document.createElement('a');
  link.href = url;
  link.download = \`\${character.characterData.name.toLowerCase().replace(/\\s+/g, '-')}.tome\`;
  link.click();
  URL.revokeObjectURL(url);
});

Import into Character Editor

// Load a .tome file into your system-specific editor
class CharacterImporter {
  async importTome(tomeFile) {
    const text = await tomeFile.text();
    const entity = TomeEntity.fromJSON(text);

    // Check if it's compatible with your system
    const system = entity.meta.system?.toLowerCase();
    if (system && !system.includes('savage worlds')) {
      const proceed = confirm(
        'This character was created for a different system. Import anyway?'
      );
      if (!proceed) return null;
    }

    // Map tome data to your editor format
    const characterData = {
      name: entity.identity.name.primary,
      concept: entity.identity.description?.short || '',
      background: entity.identity.description?.full || '',

      // Map properties back to attributes
      agility: entity.properties?.static?.agility || 'd6',
      smarts: entity.properties?.static?.smarts || 'd6',
      spirit: entity.properties?.static?.spirit || 'd6',
      strength: entity.properties?.static?.strength || 'd6',
      vigor: entity.properties?.static?.vigor || 'd6',

      // Extract skills
      skills: this.extractSkills(entity.properties?.dynamic || {}),

      // Extract edges from passive capabilities
      edges: (entity.capabilities?.passive || []).map(cap => ({
        id: cap.id,
        name: cap.name,
        effect: cap.description
      })),

      // Extract powers from action capabilities
      powers: (entity.capabilities?.actions || []).map(cap => ({
        id: cap.id,
        name: cap.name,
        effect: cap.description,
        powerPoints: this.extractCost(cap.cost),
        range: cap.range || 'Self',
        duration: cap.duration || 'Instant'
      })),

      // Map resources
      powerPoints: entity.resources?.power_points ? {
        current: entity.resources.power_points.current,
        maximum: entity.resources.power_points.maximum
      } : null,

      // Extract gear from inventory
      gear: entity.data.inventory?.items || []
    };

    return characterData;
  }

  extractSkills(dynamicProps) {
    const skills = {};
    Object.entries(dynamicProps).forEach(([key, value]) => {
      if (key.startsWith('skill_')) {
        const skillName = key.substring(6);
        skills[skillName] = value;
      }
    });
    return skills;
  }

  extractCost(costString) {
    if (!costString) return 0;
    const match = costString.match(/\\d+/);
    return match ? parseInt(match[0]) : 0;
  }
}

Advanced Techniques

Formula Evaluation

// Evaluate computed properties with formulas
class FormulaEvaluator {
  constructor(entity) {
    this.entity = entity;
  }

  evaluate(formula) {
    // Create a safe evaluation context
    const context = {
      ...this.entity.properties?.static,
      ...this.entity.properties?.dynamic
    };

    try {
      // Simple formula evaluation (use a proper library in production)
      const result = new Function(...Object.keys(context),
        \`return \${formula};\`
      )(...Object.values(context));

      return result;
    } catch (error) {
      console.error('Formula evaluation failed:', error);
      return null;
    }
  }

  // Example: Calculate HP from constitution
  calculateHP() {
    const formula = this.entity.properties?.computed?.hp;
    if (formula) {
      return this.evaluate(formula);
    }
    return 0;
  }
}

// Usage
const entity = TomeEntity.fromJSON(tomeData);
entity.setProperty('hp', 'constitution * 10 + 50', 'computed');

const evaluator = new FormulaEvaluator(entity);
const maxHP = evaluator.calculateHP(); // Returns computed value

Cross-System Conversion

// Convert between different game systems
class SystemConverter {
  // Convert D&D 5e stats to approximate Savage Worlds stats
  static dnd5eToSavageWorlds(entity) {
    if (entity.meta.system !== 'D&D 5e') {
      console.warn('Not a D&D 5e character');
    }

    const converted = entity.clone();
    converted.meta.system = 'Savage Worlds';

    // Attribute conversion (5e uses 3-20, SW uses d4-d12)
    const attrMap = {
      strength: 'strength',
      dexterity: 'agility',
      constitution: 'vigor',
      intelligence: 'smarts',
      wisdom: 'spirit'
    };

    Object.entries(attrMap).forEach(([dndAttr, swAttr]) => {
      const dndValue = entity.properties?.static?.[dndAttr] || 10;
      const swDie = this.convertStatToDie(dndValue);
      converted.setProperty(swAttr, swDie, 'static');
    });

    return converted;
  }

  static convertStatToDie(stat) {
    // 10-11 = d6 (average)
    // 12-13 = d8, 14-15 = d10, 16+ = d12
    // 8-9 = d4
    if (stat <= 9) return 'd4';
    if (stat <= 11) return 'd6';
    if (stat <= 13) return 'd8';
    if (stat <= 15) return 'd10';
    return 'd12';
  }
}

TypeScript Support

Type Definitions

import { TomeEntity, TomeData, EntityType } from 'tome';

interface Character {
  entity: TomeEntity;
  token?: TokenData;
}

interface TokenData {
  x: number;
  y: number;
  size: number;
  sprite: string;
}

// Fully typed entity creation
const character: TomeEntity = new TomeEntity({
  type: 'character' as EntityType,
  name: 'Typed Character',
  tags: ['wizard', 'human']
});

// Type-safe property access
const strength: number | undefined = character.properties?.static?.strength;

// Export with proper typing
const json: string = character.toJSON();
const data: TomeData = character.data;

Custom Entity Wrapper

// Create a type-safe wrapper for your game system
class TypedCharacter {
  private entity: TomeEntity;

  constructor(name: string) {
    this.entity = new TomeEntity({
      type: 'character',
      name,
      tags: []
    });
  }

  get name(): string {
    return this.entity.identity.name.primary;
  }

  set name(value: string) {
    this.entity.identity.name.primary = value;
  }

  getStat(stat: string): number {
    return (this.entity.properties?.static?.[stat] as number) || 0;
  }

  setStat(stat: string, value: number): void {
    this.entity.setProperty(stat, value, 'static');
  }

  getResource(name: string): { current: number; maximum: number } | null {
    return this.entity.resources?.[name] || null;
  }

  toTome(): string {
    return this.entity.toJSON();
  }

  static fromTome(json: string): TypedCharacter {
    const entity = TomeEntity.fromJSON(json);
    const character = new TypedCharacter(entity.identity.name.primary);
    character.entity = entity;
    return character;
  }
}

Best Practices

1. Always Validate System Compatibility

function isCompatibleSystem(entity, expectedSystem) {
  const system = entity.meta.system?.toLowerCase();
  if (!system) return true; // System-agnostic
  return system.includes(expectedSystem.toLowerCase());
}

2. Preserve Original Data

// Store the original tome data when importing
class ImportedCharacter {
  constructor(entity) {
    this.currentData = this.convertForApp(entity);
    this._originalTome = entity.data; // Keep original!
  }

  exportToTome() {
    // Use original as base, update changed values
    const entity = TomeEntity.fromJSON(
      JSON.stringify(this._originalTome)
    );
    this.updateEntityFromCurrent(entity);
    return entity.toJSON();
  }
}

3. Handle Missing Data Gracefully

function getResourceSafely(entity, resourceName, defaultValue = 0) {
  return entity.resources?.[resourceName]?.current ?? defaultValue;
}

function getPropertySafely(entity, propName, defaultValue = null) {
  return entity.properties?.static?.[propName]
    ?? entity.properties?.dynamic?.[propName]
    ?? defaultValue;
}

4. Use Tags for Search and Filtering

// Tag-based entity discovery
class EntityLibrary {
  constructor() {
    this.entities = [];
  }

  add(entity) {
    this.entities.push(entity);
  }

  findByTag(tag) {
    return this.entities.filter(entity =>
      entity.meta.tags?.includes(tag)
    );
  }

  findByType(type) {
    return this.entities.filter(entity =>
      entity.meta.type === type
    );
  }

  // Find all spell-casters
  findSpellcasters() {
    return this.entities.filter(entity =>
      entity.capabilities?.actions?.some(action =>
        action.id.includes('spell') || action.id.includes('magic')
      )
    );
  }
}

Community & Support

GitHub: github.com/yourusername/tome
Documentation: tome-format.dev
Discord: Join our community server for help and discussion

Contributing: Tome is an open format! If you build an integration, consider contributing it back to the community.