- Add comprehensive widget dashboard system design - Add schema system architecture with ECS pattern - Add detailed implementation plan with 8 epics - Include task breakdown with checkboxes for progress tracking - Document widget development guide - Document formula engine and YAML schema format - Add migration strategy and backward compatibility plan - Estimate 12-14 weeks total development time This branch will contain all v2.0 development work: - Widget dashboard with drag-and-drop - Schema system with YAML definitions - Formula engine with @ references - Schema-driven widgets - AI integration updates - Mobile responsive improvements Each epic builds on the previous with clear dependencies. All features designed for progressive enhancement without modes.
34 KiB
Schema System Architecture
Status: Design Phase Priority: Critical (Tier 1 Feature - 16% vote priority) Target Version: 2.0.0
Overview
The Schema System allows users to define custom RPG systems using human-readable YAML files instead of being locked into hardcoded stats. Inspired by Gemini Deep Research recommendations and Entity-Component-System (ECS) patterns.
Vision
Transform RPG Companion from a fixed D&D-style tracker into a universal RPG system that adapts to ANY tabletop game: Pathfinder, Cyberpunk RED, World of Darkness, homebrew systems, etc.
Architecture Overview
Three-Layer System
┌─────────────────────────────────────────┐
│ System Definition (YAML) │ ← Design Time
│ Rules, structure, formulas │
└─────────────────────────────────────────┘
↓ validates
┌─────────────────────────────────────────┐
│ Character Instance (JSON) │ ← Run Time
│ Actual character data │
└─────────────────────────────────────────┘
↓ renders via
┌─────────────────────────────────────────┐
│ Widget Dashboard (UI) │ ← User Interface
│ Dynamic widget rendering │
└─────────────────────────────────────────┘
System Definition Layer (YAML)
Schema Structure
# dnd5e.yaml - Example D&D 5th Edition Schema
meta:
name: "D&D 5th Edition"
version: "1.0.0"
author: "RPG Companion Community"
description: "Official D&D 5e ruleset"
tags: ["fantasy", "d20", "official"]
components:
# Core Abilities (STR, DEX, CON, etc.)
coreAbilities:
type: object
label: "Ability Scores"
icon: "🎲"
properties:
strength:
type: number
label: "Strength"
abbr: "STR"
min: 1
max: 30
default: 10
dexterity:
type: number
label: "Dexterity"
abbr: "DEX"
min: 1
max: 30
default: 10
constitution:
type: number
label: "Constitution"
abbr: "CON"
min: 1
max: 30
default: 10
intelligence:
type: number
label: "Intelligence"
abbr: "INT"
min: 1
max: 30
default: 10
wisdom:
type: number
label: "Wisdom"
abbr: "WIS"
min: 1
max: 30
default: 10
charisma:
type: number
label: "Charisma"
abbr: "CHA"
min: 1
max: 30
default: 10
# Derived Stats (calculated from abilities)
abilityModifiers:
type: object
label: "Ability Modifiers"
properties:
str_mod:
type: formula
formula: "floor((@coreAbilities.strength - 10) / 2)"
dex_mod:
type: formula
formula: "floor((@coreAbilities.dexterity - 10) / 2)"
con_mod:
type: formula
formula: "floor((@coreAbilities.constitution - 10) / 2)"
int_mod:
type: formula
formula: "floor((@coreAbilities.intelligence - 10) / 2)"
wis_mod:
type: formula
formula: "floor((@coreAbilities.wisdom - 10) / 2)"
cha_mod:
type: formula
formula: "floor((@coreAbilities.charisma - 10) / 2)"
# Resources (pools that track usage)
resources:
type: list
label: "Resources"
icon: "⚡"
items:
hitPoints:
type: resource
label: "Hit Points"
abbr: "HP"
current: 0
max:
type: formula
formula: "10 + @abilityModifiers.con_mod"
color: "#cc3333"
display: "bar"
spellSlots:
type: resource
label: "Spell Slots"
abbr: "Spells"
current: 0
max: 0
color: "#3366cc"
display: "dots"
# Skills
skills:
type: list
label: "Skills"
icon: "⚔️"
items:
acrobatics:
type: number
label: "Acrobatics"
baseAbility: "dexterity"
proficient: false
expertise: false
animalHandling:
type: number
label: "Animal Handling"
baseAbility: "wisdom"
proficient: false
expertise: false
arcana:
type: number
label: "Arcana"
baseAbility: "intelligence"
proficient: false
expertise: false
# ... more skills
# Conditions/Status Effects
statusEffects:
type: list
label: "Conditions"
icon: "✨"
items:
name:
type: text
label: "Condition Name"
duration:
type: number
label: "Rounds Remaining"
min: 0
effect:
type: text
label: "Effect Description"
# Inventory (simplified)
inventory:
type: object
label: "Equipment"
icon: "🎒"
properties:
carried:
type: list
label: "Carried Items"
worn:
type: list
label: "Worn Armor"
gold:
type: number
label: "Gold Pieces"
abbr: "GP"
default: 0
# Character Identity
identity:
type: object
label: "Character Info"
properties:
name:
type: text
label: "Name"
required: true
race:
type: text
label: "Race"
class:
type: text
label: "Class"
level:
type: number
label: "Level"
min: 1
max: 20
default: 1
background:
type: text
label: "Background"
# Prompt template for AI generation
prompts:
stats: |
Character Stats
---
HP: [@resources.hitPoints.current/@resources.hitPoints.max]
Spell Slots: [@resources.spellSlots.current/@resources.spellSlots.max]
Conditions: [List active conditions or "None"]
skills: |
Skills
---
[Skill Name]: [Modifier] | [Proficiency Status]
(List all relevant skills for the current scene)
# Widget layout suggestions
layout:
defaultTabs:
- name: "Combat"
widgets:
- type: "resources"
component: "resources"
x: 0
y: 0
w: 4
h: 3
- type: "skills"
component: "skills"
filter: ["acrobatics", "athletics", "stealth"]
x: 4
y: 0
w: 4
h: 4
- type: "statusEffects"
component: "statusEffects"
x: 8
y: 0
w: 4
h: 2
- name: "Character"
widgets:
- type: "coreAbilities"
component: "coreAbilities"
x: 0
y: 0
w: 6
h: 3
- type: "identity"
component: "identity"
x: 6
y: 0
w: 6
h: 3
Component Types
1. Object Components
Group related properties together.
identity:
type: object
properties:
name:
type: text
age:
type: number
Rendered as: Card with labeled fields
2. List Components
Collections of items.
skills:
type: list
items:
name:
type: text
value:
type: number
Rendered as: Vertical list, table, or grid
3. Resource Components
Tracked pools with current/max values.
hitPoints:
type: resource
current: 10
max: 20
display: "bar"
Rendered as: Progress bar or numeric display
4. Formula Components
Derived values calculated from other components.
armorClass:
type: formula
formula: "10 + @abilityModifiers.dex_mod + @equipment.armor.bonus"
Rendered as: Read-only calculated value
Character Instance Layer (JSON)
Instance Structure
Character data stored in extensionSettings.characterInstance:
extensionSettings.characterInstance = {
schemaId: "dnd5e-v1.0.0", // Which schema this uses
schemaVersion: "1.0.0", // Schema version
data: {
// Component data matching schema structure
coreAbilities: {
strength: 16,
dexterity: 14,
constitution: 15,
intelligence: 10,
wisdom: 12,
charisma: 8
},
abilityModifiers: {
// Calculated automatically via formula
str_mod: 3,
dex_mod: 2,
con_mod: 2,
int_mod: 0,
wis_mod: 1,
cha_mod: -1
},
resources: {
hitPoints: {
current: 12,
max: 22
},
spellSlots: {
current: 3,
max: 4
}
},
skills: [
{ name: "Acrobatics", value: 2, proficient: false },
{ name: "Athletics", value: 5, proficient: true },
{ name: "Stealth", value: 4, proficient: true }
// ... more skills
],
statusEffects: [
{ name: "Blessed", duration: 10, effect: "+1d4 to attacks" }
],
inventory: {
carried: ["Longsword", "Shield", "Healing Potion x2"],
worn: ["Chain Mail"],
gold: 47
},
identity: {
name: "Ragnar",
race: "Human",
class: "Fighter",
level: 3,
background: "Soldier"
}
},
// Metadata
createdAt: "2025-10-23T12:00:00Z",
updatedAt: "2025-10-23T14:30:00Z"
};
Formula Engine
Formula Syntax
// @ references components in character instance
@coreAbilities.strength // → 16
@abilityModifiers.str_mod // → 3
@resources.hitPoints.max // → 22
// Math operators
floor((@coreAbilities.strength - 10) / 2) // → 3
@coreAbilities.strength + 5 // → 21
(@level * 2) + @abilityModifiers.con_mod // → 8
// Conditional (future)
@coreAbilities.strength > 15 ? "Strong" : "Weak"
Safe Expression Parser
// src/systems/schema/formulaEngine.js
export class FormulaEngine {
constructor(characterData) {
this.data = characterData;
this.cache = new Map(); // Memoize calculated values
}
// Evaluate formula string
evaluate(formula) {
// Check cache first
if (this.cache.has(formula)) {
return this.cache.get(formula);
}
// Replace @ references with actual values
const resolved = this.resolveReferences(formula);
// Safe eval using Function constructor (sandboxed)
try {
const result = this.safeEval(resolved);
this.cache.set(formula, result);
return result;
} catch (error) {
console.error('[Formula Engine] Error evaluating:', formula, error);
return 0; // Fallback
}
}
// Replace @component.path with actual values
resolveReferences(formula) {
const refRegex = /@([a-zA-Z0-9_.]+)/g;
return formula.replace(refRegex, (match, path) => {
const value = this.getValueByPath(path);
return value !== undefined ? value : 0;
});
}
// Get nested value from character data
getValueByPath(path) {
const parts = path.split('.');
let value = this.data;
for (const part of parts) {
if (value && typeof value === 'object') {
value = value[part];
} else {
return undefined;
}
}
return value;
}
// Safe evaluation (whitelist functions)
safeEval(expression) {
const allowedFunctions = {
floor: Math.floor,
ceil: Math.ceil,
round: Math.round,
abs: Math.abs,
min: Math.min,
max: Math.max
};
// Create sandboxed function
const func = new Function(...Object.keys(allowedFunctions), `return ${expression}`);
// Execute with whitelisted functions
return func(...Object.values(allowedFunctions));
}
// Clear cache (call when character data changes)
invalidateCache() {
this.cache.clear();
}
}
// Usage:
const engine = new FormulaEngine(characterInstance.data);
const strMod = engine.evaluate("floor((@coreAbilities.strength - 10) / 2)");
console.log('STR Modifier:', strMod); // → 3
Schema Validation
JSON Schema Integration
Use JSON Schema to validate character instances:
// src/systems/schema/validator.js
import Ajv from 'ajv'; // Lightweight JSON Schema validator
export class SchemaValidator {
constructor() {
this.ajv = new Ajv({ allErrors: true });
}
// Convert YAML schema to JSON Schema
compileSchema(yamlSchema) {
const jsonSchema = {
type: 'object',
properties: {},
required: []
};
// Convert each component to JSON Schema property
for (const [componentName, component] of Object.entries(yamlSchema.components)) {
jsonSchema.properties[componentName] = this.convertComponent(component);
if (component.required) {
jsonSchema.required.push(componentName);
}
}
return this.ajv.compile(jsonSchema);
}
// Convert component definition to JSON Schema
convertComponent(component) {
switch (component.type) {
case 'object':
return {
type: 'object',
properties: this.convertProperties(component.properties)
};
case 'list':
return {
type: 'array',
items: this.convertComponent(component.items)
};
case 'resource':
return {
type: 'object',
properties: {
current: { type: 'number' },
max: { type: 'number' }
},
required: ['current', 'max']
};
case 'number':
return {
type: 'number',
minimum: component.min,
maximum: component.max,
default: component.default
};
case 'text':
return {
type: 'string',
minLength: component.minLength,
maxLength: component.maxLength
};
case 'formula':
// Formulas are always numbers
return { type: 'number' };
default:
return { type: 'string' };
}
}
// Validate character instance against schema
validate(characterInstance, schema) {
const compiled = this.compileSchema(schema);
const valid = compiled(characterInstance.data);
if (!valid) {
return {
valid: false,
errors: compiled.errors
};
}
return { valid: true };
}
}
Storage Layer
Hybrid Storage Strategy (Gemini Recommendation)
IndexedDB for internal operations:
- Fast local access
- Query capabilities
- No size limits (within reason)
File System Access API for import/export:
- User-friendly YAML files
- Version control compatible
- Shareable with community
// src/systems/schema/storage.js
export class SchemaStorage {
constructor() {
this.db = null;
this.init();
}
async init() {
// Initialize IndexedDB
const request = indexedDB.open('RPGCompanionSchemas', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Schemas store
if (!db.objectStoreNames.contains('schemas')) {
const schemaStore = db.createObjectStore('schemas', { keyPath: 'id' });
schemaStore.createIndex('name', 'meta.name');
schemaStore.createIndex('version', 'meta.version');
}
// Character instances store
if (!db.objectStoreNames.contains('characters')) {
const charStore = db.createObjectStore('characters', { keyPath: 'id' });
charStore.createIndex('schemaId', 'schemaId');
charStore.createIndex('name', 'data.identity.name');
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
console.log('[Schema Storage] IndexedDB initialized');
};
}
// Save schema to IndexedDB
async saveSchema(schema) {
const transaction = this.db.transaction(['schemas'], 'readwrite');
const store = transaction.objectStore('schemas');
const schemaWithId = {
id: `${schema.meta.name}-v${schema.meta.version}`,
...schema,
savedAt: new Date().toISOString()
};
await store.put(schemaWithId);
return schemaWithId.id;
}
// Load schema from IndexedDB
async loadSchema(schemaId) {
const transaction = this.db.transaction(['schemas'], 'readonly');
const store = transaction.objectStore('schemas');
return new Promise((resolve, reject) => {
const request = store.get(schemaId);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// List all schemas
async listSchemas() {
const transaction = this.db.transaction(['schemas'], 'readonly');
const store = transaction.objectStore('schemas');
return new Promise((resolve, reject) => {
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// Export schema to YAML file
async exportSchema(schemaId) {
const schema = await this.loadSchema(schemaId);
// Convert to YAML
const yaml = this.toYAML(schema);
// Use File System Access API (if available)
if ('showSaveFilePicker' in window) {
const handle = await window.showSaveFilePicker({
suggestedName: `${schema.meta.name}.yaml`,
types: [{
description: 'YAML Schema',
accept: { 'text/yaml': ['.yaml', '.yml'] }
}]
});
const writable = await handle.createWritable();
await writable.write(yaml);
await writable.close();
} else {
// Fallback: download blob
const blob = new Blob([yaml], { type: 'text/yaml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${schema.meta.name}.yaml`;
a.click();
URL.revokeObjectURL(url);
}
}
// Import schema from YAML file
async importSchema() {
// Use File System Access API (if available)
if ('showOpenFilePicker' in window) {
const [handle] = await window.showOpenFilePicker({
types: [{
description: 'YAML Schema',
accept: { 'text/yaml': ['.yaml', '.yml'] }
}]
});
const file = await handle.getFile();
const yaml = await file.text();
const schema = this.fromYAML(yaml);
// Validate and save
await this.saveSchema(schema);
return schema;
} else {
// Fallback: file input
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.yaml,.yml';
input.onchange = async (e) => {
const file = e.target.files[0];
const yaml = await file.text();
const schema = this.fromYAML(yaml);
await this.saveSchema(schema);
resolve(schema);
};
input.click();
});
}
}
// Convert schema object to YAML string
toYAML(schema) {
// Use js-yaml library
return jsyaml.dump(schema, {
indent: 2,
lineWidth: 80,
noRefs: true
});
}
// Parse YAML string to schema object
fromYAML(yaml) {
// Use js-yaml library
return jsyaml.load(yaml);
}
}
Widget Integration
Schema-Driven Widget Rendering
// src/systems/dashboard/schemaWidgets.js
export class SchemaWidgetRenderer {
constructor(schema, characterInstance, formulaEngine) {
this.schema = schema;
this.instance = characterInstance;
this.formulaEngine = formulaEngine;
}
// Render component as widget
renderComponent(componentName, container, config = {}) {
const component = this.schema.components[componentName];
const data = this.instance.data[componentName];
switch (component.type) {
case 'object':
this.renderObject(component, data, container);
break;
case 'list':
this.renderList(component, data, container, config);
break;
case 'resource':
this.renderResource(component, data, container);
break;
default:
console.warn('Unknown component type:', component.type);
}
}
// Render object component (e.g., coreAbilities)
renderObject(component, data, container) {
const html = `
<div class="schema-component schema-object">
<h4>${component.label || 'Component'}</h4>
<div class="schema-properties">
${Object.entries(component.properties).map(([key, prop]) => {
const value = data?.[key] ?? prop.default ?? '';
const displayValue = prop.type === 'formula'
? this.formulaEngine.evaluate(prop.formula)
: value;
return `
<div class="schema-property">
<label>${prop.label || key}:</label>
${prop.type === 'formula'
? `<span class="schema-value-readonly">${displayValue}</span>`
: `<input type="${prop.type === 'number' ? 'number' : 'text'}"
value="${displayValue}"
data-component="${component.label}"
data-property="${key}"
${prop.min ? `min="${prop.min}"` : ''}
${prop.max ? `max="${prop.max}"` : ''} />`
}
${prop.abbr ? `<span class="schema-abbr">(${prop.abbr})</span>` : ''}
</div>
`;
}).join('')}
</div>
</div>
`;
container.innerHTML = html;
// Add event listeners for editable fields
container.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (e) => {
this.updateProperty(
e.target.dataset.component,
e.target.dataset.property,
e.target.value
);
});
});
}
// Render list component (e.g., skills)
renderList(component, data, container, config) {
const filter = config.filter; // Optional filter for specific items
const items = Array.isArray(data) ? data : [];
const filteredItems = filter
? items.filter(item => filter.includes(item.name))
: items;
const html = `
<div class="schema-component schema-list">
<h4>${component.icon || ''} ${component.label || 'List'}</h4>
<div class="schema-list-items">
${filteredItems.map((item, index) => `
<div class="schema-list-item">
${Object.entries(component.items).map(([key, itemProp]) => {
const value = item[key] ?? '';
return `
<span class="schema-item-${key}">
${itemProp.label ? `<label>${itemProp.label}:</label>` : ''}
${itemProp.type === 'number'
? `<input type="number" value="${value}" data-index="${index}" data-key="${key}" />`
: `<span>${value}</span>`
}
</span>
`;
}).join('')}
</div>
`).join('')}
</div>
<button class="schema-add-item">+ Add ${component.label}</button>
</div>
`;
container.innerHTML = html;
// Event listeners for list item editing
container.querySelectorAll('input').forEach(input => {
input.addEventListener('change', (e) => {
this.updateListItem(
component.label,
parseInt(e.target.dataset.index),
e.target.dataset.key,
e.target.value
);
});
});
// Add item button
container.querySelector('.schema-add-item').addEventListener('click', () => {
this.addListItem(component.label);
});
}
// Render resource component (e.g., HP)
renderResource(component, data, container) {
const current = data?.current ?? 0;
const max = typeof component.max === 'object' && component.max.type === 'formula'
? this.formulaEngine.evaluate(component.max.formula)
: (data?.max ?? 0);
const percentage = max > 0 ? (current / max) * 100 : 0;
const color = component.color || '#3366cc';
const html = `
<div class="schema-component schema-resource">
<div class="schema-resource-header">
<h4>${component.label || 'Resource'}</h4>
<span class="schema-resource-values">
<input type="number" class="schema-current" value="${current}" min="0" max="${max}" />
/ ${max}
</span>
</div>
${component.display === 'bar'
? `<div class="schema-resource-bar" style="background: linear-gradient(to right, ${color}, ${color});">
<div class="schema-resource-fill" style="width: ${100 - percentage}%"></div>
</div>`
: `<div class="schema-resource-dots">
${Array(max).fill('').map((_, i) => `
<span class="schema-dot ${i < current ? 'filled' : ''}" style="background: ${color}"></span>
`).join('')}
</div>`
}
</div>
`;
container.innerHTML = html;
// Update current value
container.querySelector('.schema-current').addEventListener('change', (e) => {
this.updateResource(component.label, 'current', parseInt(e.target.value));
});
}
// Update character property
updateProperty(componentName, propertyName, value) {
if (!this.instance.data[componentName]) {
this.instance.data[componentName] = {};
}
this.instance.data[componentName][propertyName] = value;
// Invalidate formula cache
this.formulaEngine.invalidateCache();
// Save character instance
this.saveInstance();
}
// Update list item
updateListItem(componentName, index, key, value) {
if (!Array.isArray(this.instance.data[componentName])) {
this.instance.data[componentName] = [];
}
if (!this.instance.data[componentName][index]) {
this.instance.data[componentName][index] = {};
}
this.instance.data[componentName][index][key] = value;
this.saveInstance();
}
// Add new list item
addListItem(componentName) {
if (!Array.isArray(this.instance.data[componentName])) {
this.instance.data[componentName] = [];
}
// Create empty item based on component definition
const component = this.schema.components[componentName];
const newItem = {};
for (const [key, prop] of Object.entries(component.items)) {
newItem[key] = prop.default ?? '';
}
this.instance.data[componentName].push(newItem);
this.saveInstance();
// Re-render component
this.renderComponent(componentName, container);
}
// Update resource value
updateResource(componentName, field, value) {
if (!this.instance.data[componentName]) {
this.instance.data[componentName] = {};
}
this.instance.data[componentName][field] = value;
this.saveInstance();
}
// Save character instance
saveInstance() {
// Update timestamp
this.instance.updatedAt = new Date().toISOString();
// Save to extension settings
updateExtensionSettings({ characterInstance: this.instance });
// Persist to storage
saveSettings();
}
}
AI Prompt Generation
Dynamic Prompt Builder
// src/systems/generation/schemaPromptBuilder.js
export function generateSchemaPrompt(schema, characterInstance) {
let prompt = '';
// Use schema's prompt templates
if (schema.prompts) {
for (const [section, template] of Object.entries(schema.prompts)) {
// Replace [@component.path] with actual values
const resolved = resolvePromptTemplate(template, characterInstance.data);
prompt += resolved + '\n\n';
}
} else {
// Fallback: auto-generate from components
for (const [name, component] of Object.entries(schema.components)) {
prompt += generateComponentPrompt(name, component, characterInstance.data[name]);
prompt += '\n\n';
}
}
return prompt.trim();
}
// Resolve [@reference] syntax in prompt templates
function resolvePromptTemplate(template, data) {
const refRegex = /\[@([a-zA-Z0-9_.]+)\]/g;
return template.replace(refRegex, (match, path) => {
const value = getValueByPath(data, path);
return value !== undefined ? value : '[Unknown]';
});
}
// Auto-generate prompt for a component
function generateComponentPrompt(name, component, data) {
let prompt = `${component.label || name}\n---\n`;
switch (component.type) {
case 'object':
for (const [key, prop] of Object.entries(component.properties)) {
const value = data?.[key] ?? prop.default ?? '';
prompt += `${prop.label || key}: ${value}\n`;
}
break;
case 'list':
if (Array.isArray(data)) {
data.forEach(item => {
const values = Object.entries(component.items)
.map(([key, prop]) => `${prop.label || key}: ${item[key]}`)
.join(' | ');
prompt += `${values}\n`;
});
}
break;
case 'resource':
prompt += `${component.label}: ${data?.current ?? 0}/${data?.max ?? 0}\n`;
break;
}
return prompt;
}
Migration from Hardcoded to Schema
Backward Compatibility Strategy
- Keep existing hardcoded mode as fallback
- Detect schema presence to switch modes
- Provide migration wizard to convert existing characters
// src/systems/schema/migration.js
export async function migrateToSchema() {
const currentStats = extensionSettings.userStats;
const currentClassicStats = extensionSettings.classicStats;
const currentLevel = extensionSettings.level;
// Load D&D 5e schema as default
const dnd5eSchema = await schemaStorage.loadSchema('dnd5e-v1.0.0');
// Map existing data to schema
const characterInstance = {
schemaId: 'dnd5e-v1.0.0',
schemaVersion: '1.0.0',
data: {
coreAbilities: {
strength: currentClassicStats.str,
dexterity: currentClassicStats.dex,
constitution: currentClassicStats.con,
intelligence: currentClassicStats.int,
wisdom: currentClassicStats.wis,
charisma: currentClassicStats.cha
},
resources: {
hitPoints: {
current: Math.round(currentStats.health),
max: 100 // Default, user can change
}
},
identity: {
name: getContext().name1,
level: currentLevel
},
inventory: currentStats.inventory
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
// Save migrated instance
await schemaStorage.saveCharacterInstance(characterInstance);
// Enable schema mode
updateExtensionSettings({
schemaMode: true,
activeSchemaId: 'dnd5e-v1.0.0',
characterInstance
});
console.log('[Schema Migration] Successfully migrated to D&D 5e schema');
}
Schema Editor UI
Visual Builder (Future)
┌─────────────────────────────────────────────────────────┐
│ Schema Editor: D&D 5e [Save] [×]│
├─────────────────────────────────────────────────────────┤
│ ┌─ Components ──────┐ ┌─ Editor ───────────────────────┐│
│ │ + Core Abilities │ │ Component: Core Abilities ││
│ │ + Ability Mods │ │ ││
│ │ + Resources │ │ Type: [Object ▼] ││
│ │ + Skills │ │ Label: [Core Abilities] ││
│ │ + Status Effects │ │ Icon: [🎲] ││
│ │ + Inventory │ │ ││
│ │ + Identity │ │ Properties: ││
│ │ │ │ ┌──────────────────────────────┐││
│ │ [+ Add Component] │ │ │ strength │││
│ └───────────────────┘ │ │ Type: number │││
│ │ │ Label: "Strength" │││
│ │ │ Min: 1, Max: 30 │││
│ │ │ Default: 10 │││
│ │ │ [Edit] [Delete] │││
│ │ │ │││
│ │ │ dexterity │││
│ │ │ Type: number │││
│ │ │ ... │││
│ │ └──────────────────────────────┘││
│ │ [+ Add Property] ││
│ └────────────────────────────────┘│
│ │
│ [YAML View] [Visual Builder] │
└─────────────────────────────────────────────────────────┘
Success Criteria
- ✅ Users can import D&D 5e schema YAML
- ✅ Character instance validates against schema
- ✅ Formula engine calculates derived stats correctly
- ✅ Schema-driven widgets render dynamically
- ✅ Users can edit character data through widgets
- ✅ AI prompts generate based on schema
- ✅ Export/import workflows work reliably
- ✅ Backward compatibility maintained (hardcoded mode still works)
- ✅ Migration wizard converts existing characters to schema
Open Questions
- Schema Marketplace: Should we host community schemas on GitHub?
- Version Compatibility: How to handle schema version upgrades?
- Formula Complexity: Limit formula depth to prevent infinite loops?
- Multi-Character: Support multiple character instances with different schemas?
- Real-Time Sync: Should formulas recalculate on every input change or debounced?
Next Steps
- Implement YAML parser and validator
- Build formula engine with safe evaluation
- Create IndexedDB storage layer
- Develop schema-driven widget renderer
- Design schema editor UI (YAML + visual builder)
- Create D&D 5e reference schema
- Build migration wizard
- Write documentation and tutorials