Using .tome files in your applications
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.
npm install tome
# or
yarn add tome
<script type="module">
import { TomeEntity } from 'https://unpkg.com/tome/dist/index.js';
</script>
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));
}
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
Allow players to import their characters from any character editor that exports .tome files
// 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();
}
}
// 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
);
}
}
}
Create a character editor like savaged.us that exports to .tome format for portability
// 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);
});
// 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;
}
}
// 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
// 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';
}
}
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;
// 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;
}
}
function isCompatibleSystem(entity, expectedSystem) {
const system = entity.meta.system?.toLowerCase();
if (!system) return true; // System-agnostic
return system.includes(expectedSystem.toLowerCase());
}
// 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();
}
}
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;
}
// 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')
)
);
}
}
GitHub: github.com/yourusername/tome
Documentation: tome-format.dev
Discord: Join our community server for help and discussion