r/VTT • u/mpigsley • 16h ago
New tool Characters are the hardest part of building a VTT, so we open-sourced ours
It seems as though a new VTT shows up here almost every week. Maybe it's an r/unpopularopinion, but I do enjoy these posts and, even more, I just like people pushing the boundaries of what it means to tell stories online. But everyone hits the same wall with characters. Every app reimplements the same rules, and characters get stuck wherever they were made.
I’m one of the devs behind Realms of Shod. We needed character sheets, but thought that there had to be a better way. So we are open-sourcing our engine so others can avoid the pain: Open Character Format (OCF).
The short version: a pack holds a game’s rules in YAML. A character file holds only the player’s choices. You call computeSheet and get back every derived value. The sheet can’t disagree with the rules because the engine enforces that contract.
import { computeSheet, loadRuleset, startAction, type CharacterFile } from '@opencharacter/engine';
import srd from '@opencharacter/5e-srd-5.1';
const loaded = loadRuleset([srd]);
if (!loaded.ok) throw new Error(JSON.stringify(loaded.errors));
// The character file holds only the player's choices.
const character: CharacterFile = {
specVersion: '0.1.0',
id: 'quick-start',
packs: [{ id: '5e-srd-5.1', version: '0.1.0' }],
values: { '5e-srd-5.1:abilities.str.base': 16 },
inventory: [
{ id: 'sword', item: '5e-srd-5.1:weapons.longsword', equipped: true }
],
};
// Everything else is computed.
const sheet = computeSheet(loaded.ruleset, character);
sheet.values['5e-srd-5.1:abilities.str.mod']?.value; // 3
// Swing the sword.
const attack = startAction(loaded.ruleset, character, {
action: { id: '5e-srd-5.1:weapon-attack', entry: 'sword' },
params: { targetAc: 13 },
});
// attack.outcome → { result: 'hit', data: { critical: false, attack: 17 } }
// attack.proposedChanges → damage for the target, for your app to apply
Full walkthrough, including character changes and translations, in the quick start.
A few things I’m proud of:
- Homebrew is just another pack. It loads beside the official ones; no forking.
- The engine tells you what to roll and pauses while your app rolls the dice however it wants.
- Versioned packs ship upgrade scripts, so old characters keep loading after rules change.
Packs so far: 5E SRD, ORC 2e, Fate Core, Forged in the Dark, MÖRK BORG, Mothership. TypeScript reference engine; the spec, schemas, and conformance tests are language-neutral if you want to port it.
It’s early (v0.1). I’d love feedback from anyone building a VTT or character tool, and pack authors are very welcome.
Obligatory xkcd 927.
Site: opencharacterformat.org · Repo: github.com/pixelqueue/opencharacter