Skip to content

System

A System entity describes a game system (e.g. D&D 5e, Pathfinder 2e, etc.) and its associated traits. Systems contain various traits that are inherited and extended by Campaigns. When a Campaign is created, it can optionally be associated with a System, in which case it will inherit all of the System's traits. Campaigns can also have their own traits in addition to the inherited System traits, and they can override any of the inherited traits if they choose.

Systems not only have an entity definition, but also a fixture containing a version of that system's actual values. The app ships with a number of systems pre-loaded, and others can be loaded using fixtures of the same type.

The entity definition is a traditional data object, with members (properties and methods) of various types, meant to be persisted in some kind of database, exposed via API, etc. However, part of a System is logic, which is best implemented not as data, but as code (for instance: dnd5e-2014.ts). This is part of the totality of a logical System, but is somewhere between a data entity and a fixture, in terms of implementation.


Entity Definition

A System is composed of three conceptual layers:

  1. Core metadata — identity, versioning, and display info (stored in the systems DB table)
  2. Campaign trait definitions — collections of data items that Campaigns inherit (stored in trait-specific DB tables with system_id ownership)
  3. Character logic module — validation, suggestions, and class/ability-score rules (implemented as TypeScript code in packages/shared/src/systems/)

These layers are unified at runtime but have different storage and delivery mechanisms, reflecting the reality that some parts of a System are pure data while others are inherently code.

Layer 1: Core Metadata

The foundation of a System as a database entity. Stored in the systems table.

Field Type Description
id string (PK) Unique identifier, e.g. 'dnd5e-2014', 'system8'
name string Display name, e.g. 'D&D 5th Edition (2014)'
foundation string Dice foundation, e.g. 'd20' (default)
description string? Brief description for UI display
apiUrl string? External API endpoint (e.g. 'https://www.dnd5eapi.co')
createdAt string ISO timestamp
updatedAt string? ISO timestamp

Current systems shipped: dnd5e-2014, dnd5e-2024, system8, custom

Layer 2: Campaign Trait Definitions

These are data collections that define the defaults a Campaign inherits when it selects a System. Each trait type is stored in its own database table with a dual-ownership pattern:

  • System-level rows: system_id is set, campaign_id is NULL → read-only defaults
  • Campaign-level rows: campaign_id is set, system_id is NULL → user-editable overrides

When a Campaign's traits are fetched via API, both sets are merged and annotated with runtime flags (isSystemDefault, isEditable).

Trait Types

Trait Type DB Table Description Example
Property Types prop_types Custom data types for token properties Ability Score (renders with modifier in parens)
Property Definitions property_definitions Named properties tokens can have, each referencing a prop type Strength (type: ability-score, category: abilities)
Conditions conditions Status effects with SVG badge icons Blinded, Prone, Stunned
Bars bars Visual bar indicators on tokens HP bar (green gradient, value = hp/maxHp)
Light Source Types light_source_types Predefined light sources for vision/lighting Torch (bright 4, dim 8)
Vision Types vision_types Vision modes with darkness/concealment flags Darkvision (60ft, ignores darkness)
Campaign Functions campaign_functions JavaScript functions available in Action code lightDamage(whichAbilityScore)

Shared Schema Pattern

Every trait table follows the same ownership columns:

id           TEXT PRIMARY KEY
...trait-specific fields...
system_id    TEXT  -- nullable; set for system-level defaults
campaign_id  TEXT  -- nullable; set for campaign-level overrides
created_at   TEXT NOT NULL
updated_at   TEXT

Campaign Inheritance Behavior

Scenario What Happens
Campaign has systemId GET endpoint returns system_id rows merged with campaign_id rows. System rows are marked isSystemDefault: true, isEditable: false.
Campaign has no systemId GET endpoint returns only campaign_id rows, all marked isEditable: true.
User changes system A confirmation dialog offers: (1) copy new system's defaults as editable campaign properties, (2) delete campaign properties matching old system's defaults, (3) allow incoming to overwrite existing. Server handles via POST /api/campaigns/:id/change-system.

Prop Types

Field Type Description
id string (PK) Unique identifier
name string Display name (e.g. 'Ability Score', 'Number', 'Text')
renderFunction string? JavaScript expression for custom display. Receives value, returns formatted string.

Property Definitions

Field Type Description
id string (PK) Unique identifier
name string Property name (e.g. 'Strength', 'HP')
propType string References a prop type ID or built-in type ('number', 'string', 'boolean')
category string? Grouping (e.g. 'abilities', 'combat')
order integer Sort order within category

Conditions

Field Type Description
id string (PK) Unique identifier
name string Condition name (e.g. 'Blinded')
description string? Rules text
badgeSvg string SVG icon displayed on tokens
sortOrder integer Display order

Bars

Field Type Description
id string (PK) Unique identifier
name string Display name (e.g. 'HP')
color string Any valid CSS background, including gradients
valueFunction string JavaScript expression receiving metadata, returning 0–1
order integer Display order (lower = top)

Light Source Types

Field Type Description
id string (PK) Unique identifier
name string Display name (e.g. 'Torch')
brightRadius integer Bright light radius in grid squares
dimRadius integer Dim light radius in grid squares
shape string 'circle' or 'cone'
coneAngle float Angle in degrees (for cone shape)
colorType string Color mode (e.g. 'incandescent', 'cool')
colorTemperature float? Kelvin temperature
color string? CSS color override

Vision Types

Field Type Description
id string (PK) Unique identifier
name string Display name (e.g. 'Darkvision')
ignoresDarkness boolean Can see in darkness
ignoresConcealment boolean Ignores concealment
ignoresCover boolean Ignores cover
range integer? Range in feet

Campaign Functions

Field Type Description
id string (PK) Unique identifier
name string Function name used in Action code
args string Comma-separated argument names (e.g. 'whichAbilityScore')
functionType string 'global' (top-level) or 'token' (method on token proxy)
code string JavaScript function body
description string? Human-readable description
sortOrder integer Display order

Layer 3: Character Logic Module

The code layer of a System, implemented as TypeScript modules in packages/shared/src/systems/. These are not stored in the database—they ship with the application and are lazy-loaded at runtime via the systemRegistry.

SystemModule Interface

interface SystemModule {
  id: string;
  name: string;
  version: string;
  description: string;
  abilityScores: AbilityScoreConfig;
  classes?: SystemClass[];
  validateHp?: (character: Character) => HpValidation;
  suggestions?: SuggestionEngine;
  classAttributePriorities?: ClassAttributePriorities[];
}

AbilityScoreConfig

Defines how ability scores work in this system. Every system must provide this.

interface AbilityScoreConfig {
  abilities: string[];                    // e.g. ['Strength', 'Dexterity', ...]
  standardArray?: number[];               // e.g. [15, 14, 13, 12, 10, 8]
  maxBaseScore: number;                   // Before magic items (5e: 20, S8: 18)
  maxScore: number;                       // Absolute max (5e: 30, S8: 99)
  minScore: number;                       // Floor (typically 1)
  getMaxBaseScoreAtLevel(level): number;  // Level-dependent cap
  validateAbilityScores(character): AbilityScoreValidation;
  validateSingleAbility(character, abilityName): ValidationIssue[];
}

SystemClass

Describes a character class available in the system.

interface SystemClass {
  name: string;
  description?: string;
  hitDie?: number;        // e.g. 12 for Barbarian, 6 for Wizard
}

SuggestionEngine

Generates character-building recommendations based on class, level, and current stats.

interface SuggestionEngine {
  suggestAttributes(context: SuggestionContext): AttributeSuggestion[];
  suggestFeats(context: SuggestionContext): FeatSuggestion[];
  suggestPowers(context: SuggestionContext): PowerSuggestion[];
}

Each function returns prioritized suggestions with human-readable explanations.

ClassAttributePriorities

Maps each class to its preferred ability scores, used by the suggestion engine.

interface ClassAttributePriorities {
  className: string;
  primary: string[];     // Most important stats
  secondary: string[];   // Useful but not critical
  tertiary?: string[];   // Situationally useful
}

How the Three Layers Connect

┌─────────────────────────────────────────────────────────────────────┐
│                         System                                      │
│                                                                     │
│  ┌─────────────────┐  ┌─────────────────────┐  ┌────────────────┐  │
│  │  Layer 1: Core   │  │  Layer 2: Traits     │  │  Layer 3: Logic│  │
│  │  (systems table) │  │  (trait tables)       │  │  (TS modules)  │  │
│  │                  │  │                       │  │                │  │
│  │  id              │  │  prop_types           │  │  abilityScores │  │
│  │  name            │  │  property_definitions │  │  classes        │  │
│  │  description     │  │  conditions           │  │  validateHp    │  │
│  │  foundation      │  │  bars                 │  │  suggestions   │  │
│  │  apiUrl          │  │  light_source_types   │  │  classPriority │  │
│  │                  │  │  vision_types         │  │                │  │
│  │                  │  │  campaign_functions    │  │                │  │
│  └─────────────────┘  └─────────────────────┘  └────────────────┘  │
│                                                                     │
│  Stored in DB ──────────────────────────────────  Shipped as code   │
│  Served via REST API ───────────────────────────  Lazy-loaded JS    │
│  Seeded by SQL migrations ──────────────────────  Imported by UI    │
└─────────────────────────────────────────────────────────────────────┘

Campaign relationship: A Campaign references a System via systemId. The Campaign inherits Layer 2 traits (merged at query time) and uses Layer 3 logic for character validation and suggestions. Layer 1 metadata is used for UI display.

Character relationship: A Character references a System via systemId. The Character Sheet loads the Layer 3 module to validate ability scores, HP, and generate optimization suggestions.


Character Trait Definitions

Characters in this system have structured traits that are defined by the System. While the Character entity itself is system-agnostic (it stores raw data), the System provides the rules for interpreting and validating that data.

Character Entity Structure

interface Character {
  id: string;
  userId: string;
  name: string;
  level: number;
  systemId: string;

  // Build
  classes: string[];                    // Class names (e.g. ['Fighter', 'Wizard'])
  classLevels?: CharacterClassLevel[];  // Detailed multiclass levels
  origins: string[];                    // Race/background/origin

  // Core data
  attributes: CharacterAttribute[];     // With component breakdown
  proficiencyBonus: number;             // Computed from level
  skills: CharacterSkill[];
  feats: CharacterFeat[];
  powers: CharacterPower[];
  attacks: CharacterAttack[];

  // Art
  artworks?: ArtworkEntity[];

  // System-specific
  metadata?: Record<string, unknown>;   // e.g. { abilityScoreMode, hpMode }
}

Trait Type Breakdown

Attributes (Ability Scores)

Attributes use a component breakdown model where the total value is the sum of named components. This allows tracking the source of each bonus.

interface CharacterAttribute {
  name: string;                          // e.g. 'Strength'
  components: AttributeComponent[];      // [{name:'base',value:15}, {name:'asi',value:2}]
}

Component names are freeform but conventionally include: base (from array/point-buy/roll), ancestry/racial (from race/origin), asi (from ability score increases), magic/item (from equipment). The System's validation logic examines these component names to enforce rules.

System defines: Which abilities exist (abilities[]), their bounds (maxBaseScore, maxScore, minScore), generation modes (standard array, point-buy, rolled), ASI progression, and all validation rules.

Skills

interface CharacterSkill {
  name: string;          // e.g. 'Athletics'
  attribute: string;     // Linked ability score, e.g. 'Strength'
  proficient: boolean;
  expertise: boolean;
  brief?: string;
  level?: number;        // Level at which proficiency was gained
}

System should define: Available skills, their default attributes, and which classes grant proficiency in which skills at which levels.

Feats

interface CharacterFeat {
  name: string;
  description?: string;
  pact?: string;         // Source pact (for Warlock-style systems)
  source?: string;       // Where it came from (class, origin, etc.)
  level?: number;        // Level gained
  slotIndex?: number;    // Slot within a level (for multiple feats at same level)
}

System should define: Available feats with prerequisites, which levels offer feat choices (vs ASI), and any class-specific feat lists (e.g. Origin Feats in 5e 2024, Talents in System 8).

Powers (Spells / Class Features)

interface CharacterPower {
  name: string;
  brief?: string;        // Short description
  school?: string;       // Magic school or discipline
  rank?: number;         // Spell level / power rank (1–5 for System 8)
  type?: string;         // Category (e.g. 'spell', 'cantrip', 'class-feature')
  className?: string;    // Source class (for multiclass)
  url?: string;          // Canonical URL for lookup
  level?: number;        // Level at which gained
}

System should define: Available powers per class, rank/level progression, and which powers are available at which character levels.

Attacks

interface CharacterAttack {
  name: string;
  type: string;              // 'Natural Weapon', 'Spellcasting Implement', etc.
  proficient: boolean;
  attribute: string;         // Linked ability score
  damage?: DamageWeight;     // 'light' | 'medium' | 'heavy' (System 8)
  hit: number;               // Flat attack bonus
  hitPower: number;          // Added per die (System 8)
  crit: number;              // Crit range expansion
  critPower: number;         // Crit multiplier bonus (System 8)
  damageDice?: string;       // e.g. '2d6' (5e)
  damageType?: string;       // e.g. 'slashing' (5e)
  damageAttribute?: string;  // Ability added to damage
  range?: string;            // e.g. '30/120 ft'
}

System should define: Weapon types, damage dice/weights, proficiency rules, and how attack/damage bonuses are calculated.

Inventory Items (Future)

Not yet implemented. Expected to include: name, weight, cost, type, properties, equipped/unequipped state. Systems would define equipment lists and encumbrance rules.


System Fixture Definition

A System Fixture is the complete dataset that defines a concrete game system. It combines all three layers into a single deliverable that can be:

  1. Shipped with the app (built-in systems like D&D 5e, System 8)
  2. Loaded as a package (community systems, homebrew)

Fixture Structure

A fixture has two halves:

Data Half (Trait Defaults)

Expressed as SQL migrations or JSON import files. Contains rows for each trait table:

fixtures/
  dnd5e-2014/
    metadata.json           # Core metadata (name, description, foundation)
    prop-types.json         # Default prop types (Number, Text, Ability Score)
    property-definitions.json  # Default properties (Str, Dex, ..., HP, AC)
    conditions.json         # Default conditions (Blinded, Charmed, ...)
    bars.json               # Default bars (HP)
    light-source-types.json # Default light sources (Torch, Lantern, ...)
    vision-types.json       # Default vision types (Normal, Darkvision, ...)
    campaign-functions.json # Default functions (if any)

For built-in systems, these are currently seeded via SQL migration files (migrations 0014–0018). For user-installable systems, they would be imported via a fixture loader.

Code Half (Logic Module)

Expressed as a TypeScript module implementing SystemModule:

packages/shared/src/systems/
  dnd5e-2014.ts     # Implements SystemModule for D&D 5e 2014
  dnd5e-2024.ts     # Implements SystemModule for D&D 5e 2024
  system8.ts        # Implements SystemModule for System 8

The code half is inherently tied to the application codebase. For community systems, a plugin architecture would be needed (future consideration).

Packaging Considerations

The challenge: a System fixture contains both data (easily serializable, importable) and code (needs to execute in a JS runtime, has dependencies on the Character type system). Options include:

Approach Pros Cons
Current: SQL seeds + TS modules Simple, type-safe, fast Not portable, requires app rebuild for new systems
JSON data + sandboxed JS Portable, user-installable Security concerns, limited API surface
WASM plugins Safe, performant Complex, heavy tooling
Hybrid: data = importable, logic = contributor PR Best of both, maintains type safety Still requires process for logic contributions

The recommended near-term path is the hybrid approach: trait data is fully importable/exportable as JSON fixtures, while logic modules are contributed to the codebase and shipped with the app.


System Fixtures: D&D 5e 2014 & 2024

D&D 5e 2014

Metadata

Field Value
id dnd5e-2014
name D&D 5th Edition (2014)
foundation d20
description Dungeons & Dragons 5th Edition rules from the 2014 release.
apiUrl https://www.dnd5eapi.co

Campaign Trait Defaults

Prop Types (3):

Name Render Function
Number (none)
Text (none)
Ability Score value => `${value} (${Math.floor((value - 10) / 2) >= 0 ? "+" : ""}${Math.floor((value - 10) / 2)})`

Property Definitions (10):

Name Prop Type Category Order
Strength ability-score abilities 1
Dexterity ability-score abilities 2
Constitution ability-score abilities 3
Intelligence ability-score abilities 4
Wisdom ability-score abilities 5
Charisma ability-score abilities 6
HP number combat 1
Max HP number combat 2
AC number combat 3
Speed number combat 4

Conditions (15): Blinded, Charmed, Deafened, Frightened, Grappled, Incapacitated, Invisible, Paralyzed, Petrified, Poisoned, Prone, Restrained, Stunned, Unconscious, Exhaustion

Bars (1): HP bar — green gradient, (metadata?.hp ?? 0) / (metadata?.maxHp ?? 1)

Light Sources (8): Torch (4/8), Hooded Lantern (6/12), Bullseye Lantern (12/24 cone 90°), Candle (1/2), Light cantrip (4/8), Dancing Lights (0/2), Daylight (12/24), Continual Flame (4/8)

Vision Types (10): Normal, Darkvision 60ft, Superior Darkvision 120ft, Blindsight 10/30/60ft, Truesight 30/60/120ft, Tremorsense 30/60ft

Campaign Functions: (none — D&D 5e uses standard dice expressions rather than system-specific formulas)

Character Logic Module

Ability Score Config:

  • Abilities: Strength, Dexterity, Constitution, Intelligence, Wisdom, Charisma
  • Standard Array: 15, 14, 13, 12, 10, 8
  • Point Buy: 27 points, scores 8–15
  • Max Base Score: 20 (before magic); Max Score: 30 (with magic)
  • ASI Levels: 4, 8, 12, 16, 19 (+Fighter 6/14, +Rogue 10)
  • Each ASI: +2 to one score OR +1/+1 to two scores, OR take a Feat

Classes (12):

Class Hit Die Primary Secondary
Barbarian d12 Str, Con Dex
Bard d8 Cha Dex, Con
Cleric d8 Wis Con, Str
Druid d8 Wis Con, Dex
Fighter d10 Str, Con Dex
Monk d8 Dex, Wis Con
Paladin d10 Str, Cha Con
Ranger d10 Dex, Wis Con
Rogue d8 Dex Int, Con
Sorcerer d6 Cha Con, Dex
Warlock d8 Cha Con, Dex
Wizard d6 Int Con, Dex

HP Validation: Level 1 = max hit die + CON mod. Level 2+ = previous + average (rounded up) + CON mod. Validates against class hit die and warns on deviation.

Suggestion Engine: Suggests ASI targets based on class priorities, feat recommendations (Lucky, GWM, Sharpshooter, War Caster, etc.), and spell/cantrip picks per class.

D&D 5e 2024

Differences from 2014

Aspect 2014 2024
Origin Feats None Every character gets a feat at level 1
Ability bonuses From race (+2/+1) From background (+2/+1)
Weapon Mastery Not present Weapon Mastery properties
Feat vs ASI Equal weight Default to ASI until primary stat ≥ 20

Campaign trait defaults (prop types, property definitions, conditions, bars, light sources, vision types) are identical to 2014. The differences are entirely in the character logic module.

Suggestion engine additions: Suggests Origin Feats (Alert, Magic Initiate, Skilled) at level 1; explicitly recommends ASI over feats when primary stat < 20.

System 8

Metadata

Field Value
id system8
name System 8
foundation d20
description A streamlined RPG system with per-attribute proficiency bonuses.
apiUrl https://purplecat.wiki

Campaign Trait Defaults

Prop Types (3): Number, Text, Ability Score (same render function as 5e)

Property Definitions (10): (same as 5e: 6 abilities + HP/MaxHP/AC/Speed)

Conditions (5): Dazed, Stunned, Prone, Slowed, Weakened (simplified set)

Bars (1): HP bar (same formula as 5e)

Light Sources (3): Torch (4/8), Lantern (6/12), Light Spell (6/12)

Vision Types (3): Normal, Darkvision (60ft), Blindsight (30ft)

Campaign Functions (4):

Function Type Args Description
lightDamage token whichAbilityScore {rank}d4+{abilityBonus} — dex-based light attack
mediumDamage token whichAbilityScore {rank}d6+{abilityBonus} — balanced attack
heavyDamage token whichAbilityScore {rank}d8+{abilityBonus} — str-based heavy attack
attackRoll token whichAbilityScore 1d20+{abilityBonus+profBonus} — attack roll

(rank = Math.ceil(level / 4), abilityBonus from token.metadata[whichAbilityScore])

Character Logic Module

Ability Score Config:

  • Same 6 abilities as 5e
  • Standard Array: 16, 14, 12, 12, 10, 10 (sum = 74)
  • Max Base Score: 18 (not 20 — proficiency compensates); Max Score: 99
  • Trade rule: May reduce one 10→8 to raise a 12 or 14 by +2 (max one 8)
  • Proficiency system: Per-attribute proficiency bonuses. Each level (+ 3 starting levels) grants +1 to 2 different abilities. Total points = (level + 3) × 2. Per-ability cap = level + 3.
  • No point-buy. Modes: standard-array, rolled, custom.

Classes (9):

Class Hit Die Primary Secondary
Fighter d10 Str, Con Dex
Rogue d8 Dex Int, Con
Arcanist d6 Int Con, Dex
Elementalist d8 Int, Con Dex
Priest d8 Wis Con, Cha
Witch d6 Wis, Cha Con
Warlock d8 Cha Con, Dex
Psion d6 Int, Wis Con
Artificer d8 Int Con, Dex

HP Validation: Not implemented (no validateHp on SystemModule). HP is implied to follow the same average-per-level pattern.

Suggestion Engine: Suggests attribute increases based on class priorities. Feats are a placeholder (System 8 uses "Talents" instead). Power suggestions include class-specific abilities (Arcane Bolt, Sacred Light, Mind Thrust, etc.).


Runtime Architecture

System Registry

Systems are registered in packages/shared/src/systems/index.ts:

const systemRegistry: SystemModuleRegistry = {
  'dnd5e-2014': async () => (await import('./dnd5e-2014.js')).dnd5e2014,
  'dnd5e-2024': async () => (await import('./dnd5e-2024.js')).dnd5e2024,
  'system8':    async () => (await import('./system8.js')).system8,
};

Modules are lazy-loaded and cached. The registry also provides systemInfoList for UI display without loading full modules.

API Surface

Endpoint Method Description
/api/systems GET List all systems (metadata only)
/api/campaigns/:id GET Campaign with systemId reference
/api/campaigns/:id/change-system POST Change system with merge/delete options
/api/campaigns/:id/prop-types GET Merged system + campaign prop types
/api/campaigns/:id/property-definitions GET Merged system + campaign definitions
/api/campaigns/:id/bars GET Merged system + campaign bars
/api/campaigns/:id/functions GET Merged system + campaign functions
/api/campaigns/:id/light-source-types GET Merged system + campaign light sources
/api/campaigns/:id/vision-types GET Merged system + campaign vision types
/api/conditions?campaignId=X GET Merged system + campaign conditions

All trait GET endpoints follow the same pattern: fetch campaign's systemId, query with OR(campaignId, systemId), annotate with isSystemDefault/isEditable.

All trait POST/PUT/DELETE endpoints enforce that system-level rows cannot be modified (403 if systemId is set and campaignId is null).


TODO

  • [x] Define the basic System entity members, based on current implementation. Then, plan ahead for the next three TODOs, which will add quite a bit to this object; consider how this object's structure can be digestible with so many members/submembers/etc.
  • [x] Considering the existing defintion of Campaign traits which can be inherited from a System (property types, property definitions, conditions, light sources, vision types, and functions), define these same traits at the System level
  • [x] Consider also the defined traits of a Character as implemented, such as attributes (HP, Strength, etc), skills, feats, spells/powers, and inventory items, all of which should have their own definitions, with System having members that are collections of these.
  • [x] Considering the System-specific logic for character sheet validation and optimization (defined in dnd5e-2014.ts, for instance), define how these members fit into the overall System entity definition. Most of this part of a logical system is best defined as code, not data, so it won't need a data definition per se--rather something like an interface--but also we must consider how to package a data fixture with a bunch of code.
  • [x] Considering what was previously defined in the last TODO, define the System Fixtures for D&D 5e 2014 and 2024, with one or two examples of each trait type (both the campaign-inherited ones and the character traits).