Storyteller Suite - Template System
Overview
The template system enables:
- Prebuilt templates for common entity types
- Full world templates with interconnected entities
- Custom user templates
- Variable substitution (
{{variable}}) - Template versioning and migration
Template Types
| Type | Description | Example |
|---|---|---|
| Single Entity | One entity | Character template |
| Entity Set | Related entities | Party of adventurers |
| Full World | Complete setting | Fantasy Kingdom |
| Story Structure | Narrative framework | Three-Act Structure |
Template Structure
TemplateTypes.ts
interface Template {
id: string;
name: string;
description?: string;
category: TemplateCategory;
version: string;
author?: string;
isBuiltIn: boolean;
// Content
entities: TemplateEntities;
variables?: TemplateVariable[];
placeholders?: TemplatePlaceholder[];
// Metadata
metadata?: {
entityCounts: Record<TemplateEntityType, number>;
setupInstructions?: string;
recommendedSettings?: Record<string, any>;
};
// Usage tracking
usageCount?: number;
lastUsed?: string;
}
interface TemplateEntities {
characters?: TemplateCharacter[];
locations?: TemplateLocation[];
events?: TemplateEvent[];
items?: TemplateItem[];
groups?: TemplateGroup[];
maps?: TemplateMap[];
chapters?: TemplateChapter[];
scenes?: TemplateScene[];
cultures?: TemplateCulture[];
economies?: TemplateEconomy[];
magicSystems?: TemplateMagicSystem[];
}
type TemplateEntityType =
| 'character'
| 'location'
| 'event'
| 'item'
| 'group'
| 'map'
| 'chapter'
| 'scene'
| 'culture'
| 'economy'
| 'magicSystem';
Variables
Templates support {{variable}} placeholders:
interface TemplateVariable {
name: string;
label: string;
type: 'text' | 'number' | 'date' | 'select' | 'boolean';
defaultValue?: any;
required?: boolean;
options?: string[]; // For 'select' type
helpText?: string;
usedIn?: VariableUsage[]; // Where it's used
}
interface VariableUsage {
entityType: TemplateEntityType;
entityTemplateId: string;
field: string;
}
Example Variable Usage
const template: Template = {
name: "Custom Kingdom",
variables: [
{
name: "kingdomName",
label: "Kingdom Name",
type: "text",
required: true,
usedIn: [
{ entityType: 'location', entityTemplateId: 'LOC_001', field: 'name' },
{ entityType: 'group', entityTemplateId: 'GROUP_001', field: 'description' }
]
},
{
name: "foundingYear",
label: "Founding Year",
type: "number",
defaultValue: 1000
}
],
entities: {
locations: [{
templateId: 'LOC_001',
name: "Kingdom of {{kingdomName}}",
description: "Founded in {{foundingYear}}..."
}]
}
};
VariableSubstitution.ts
class VariableSubstitution {
static substituteString(
text: string,
values: TemplateVariableValues,
strictMode: boolean = false
): SubstitutionResult {
let result = text;
const warnings: string[] = [];
const errors: string[] = [];
// Find all {{variable}} patterns
const matches = text.matchAll(/\{\{(\w+)\}\}/g);
for (const match of matches) {
const variableName = match[1];
const value = values[variableName];
if (value !== undefined) {
result = result.replace(match[0], String(value));
} else if (strictMode) {
errors.push(`Variable "${variableName}" not found`);
} else {
warnings.push(`Variable "${variableName}" not found, leaving as-is`);
}
}
return { success: errors.length === 0, value: result, warnings, errors };
}
static substituteEntity<T>(
entity: T,
values: TemplateVariableValues,
strictMode: boolean = false
): SubstitutionResult {
const cloned = JSON.parse(JSON.stringify(entity));
return this.substituteObject(cloned, values, strictMode);
}
}
Prebuilt Templates
Located in src/templates/prebuilt/:
Character Templates
// CharacterTemplates.ts
export const heroTemplate: Template = {
id: 'character-hero',
name: 'Classic Hero',
category: 'singleEntity',
entities: {
characters: [{
templateId: 'CHAR_HERO',
name: '{{heroName}}',
description: 'A brave {{heroType}} destined for greatness',
traits: ['Brave', 'Honorable', 'Determined'],
status: 'Alive',
backstory: '...'
}]
},
variables: [
{ name: 'heroName', label: 'Hero Name', type: 'text', required: true },
{ name: 'heroType', label: 'Hero Type', type: 'select',
options: ['warrior', 'mage', 'rogue', 'healer'] }
]
};
Location Templates
// LocationTemplates.ts
export const castleTemplate: Template = {
id: 'location-castle',
name: 'Medieval Castle',
entities: {
locations: [
{ templateId: 'CASTLE', name: '{{castleName}}', locationType: 'Castle' },
{ templateId: 'THRONE_ROOM', name: 'Throne Room', locationType: 'Room',
parentLocationId: 'CASTLE' },
{ templateId: 'DUNGEON', name: 'Dungeon', locationType: 'Room',
parentLocationId: 'CASTLE' }
]
}
};
Full World Templates
// FantasyKingdom.ts
export const fantasyKingdomTemplate: Template = {
id: 'world-fantasy-kingdom',
name: 'Fantasy Kingdom',
category: 'fullWorld',
description: 'Complete medieval fantasy setting with royalty, magic, and intrigue',
entities: {
characters: [
{ templateId: 'CHAR_001', name: 'King Aldric', ... },
{ templateId: 'CHAR_002', name: 'Queen Elena', ... },
{ templateId: 'CHAR_003', name: 'Prince Marcus', ... },
// ... 12 characters total
],
locations: [
{ templateId: 'LOC_001', name: 'Castle Stormhaven', ... },
{ templateId: 'LOC_002', name: 'Throne Room', ... },
// ... 8 locations
],
events: [
{ templateId: 'EVENT_001', name: 'Coronation of King Aldric', ... },
// ... 5 events
],
groups: [
{ templateId: 'GROUP_001', name: 'The Royal House', ... },
{ templateId: 'GROUP_002', name: 'The City Watch', ... },
{ templateId: 'GROUP_003', name: 'Merchants Guild', ... }
],
cultures: [
{ templateId: 'CULTURE_001', name: 'Kingdom of Arendor', ... }
],
economies: [
{ templateId: 'ECONOMY_001', name: 'Feudal Trade Economy', ... }
],
magicSystems: [
{ templateId: 'MAGIC_001', name: 'Elemental Magic', ... }
],
items: [
{ templateId: 'ITEM_001', name: 'Crown of Arendor', isPlotCritical: true }
]
},
metadata: {
entityCounts: {
character: 12, location: 8, event: 5, item: 1,
group: 3, culture: 1, economy: 1, magicSystem: 1
},
setupInstructions: 'This template creates a complete fantasy kingdom...'
}
};
Story Structure Templates
// StoryStructureTemplates.ts
export const threeActStructure: Template = {
id: 'structure-three-act',
name: 'Three-Act Structure',
category: 'storyStructure',
entities: {
chapters: [
{ templateId: 'ACT_1', name: 'Act 1: Setup', number: 1 },
{ templateId: 'ACT_2', name: 'Act 2: Confrontation', number: 2 },
{ templateId: 'ACT_3', name: 'Act 3: Resolution', number: 3 }
],
scenes: [
{ templateId: 'SCENE_HOOK', name: 'Opening Hook', chapterId: 'ACT_1' },
{ templateId: 'SCENE_INCITING', name: 'Inciting Incident', chapterId: 'ACT_1' },
// ... more scenes
]
}
};
Template Application
TemplateApplicator.ts
class TemplateApplicator {
async applyTemplate(
template: Template,
variableValues: TemplateVariableValues,
options: ApplyOptions = {}
): Promise<ApplyResult> {
const result: ApplyResult = {
success: true,
created: { characters: [], locations: [], events: [], ... },
errors: [],
warnings: []
};
// Substitute variables
const substitutedEntities = this.substituteVariables(
template.entities,
variableValues
);
// Create entities in order (locations first for hierarchy)
await this.createLocations(substitutedEntities.locations, result);
await this.createCharacters(substitutedEntities.characters, result);
await this.createGroups(substitutedEntities.groups, result);
await this.createEvents(substitutedEntities.events, result);
await this.createItems(substitutedEntities.items, result);
await this.createChapters(substitutedEntities.chapters, result);
await this.createScenes(substitutedEntities.scenes, result);
// Link entities using templateId mapping
await this.linkEntities(result);
return result;
}
private async linkEntities(result: ApplyResult): Promise<void> {
// Map templateId → actual entity id
const idMap = new Map<string, string>();
for (const [type, entities] of Object.entries(result.created)) {
for (const entity of entities) {
if (entity.templateId) {
idMap.set(entity.templateId, entity.id);
}
}
}
// Update references
for (const character of result.created.characters) {
if (character.groups) {
character.groups = character.groups.map(g => idMap.get(g) || g);
}
await this.plugin.saveCharacter(character);
}
// ... similar for other entity types
}
}
Template Storage
TemplateStorageManager.ts
class TemplateStorageManager {
private templateFolder = 'StorytellerSuite/Templates';
async saveTemplate(template: Template): Promise<void> {
const path = `${this.templateFolder}/${template.id}.json`;
const content = JSON.stringify(template, null, 2);
await this.vault.adapter.write(path, content);
}
async loadTemplates(): Promise<Template[]> {
const builtIn = this.getBuiltInTemplates();
const custom = await this.loadCustomTemplates();
return [...builtIn, ...custom];
}
async deleteTemplate(templateId: string): Promise<void> {
const path = `${this.templateFolder}/${templateId}.json`;
await this.vault.adapter.remove(path);
}
}
Template Conversion
NoteToTemplateConverter.ts
Convert existing note to template:
class NoteToTemplateConverter {
async convertNote(file: TFile): Promise<Template> {
const content = await this.vault.cachedRead(file);
const { frontmatter, sections } = this.parseMarkdown(content);
// Detect entity type from folder
const entityType = this.detectEntityType(file.path);
// Build template
return {
id: `note-${Date.now()}`,
name: frontmatter.name || file.basename,
category: 'singleEntity',
isBuiltIn: false,
entities: {
[entityType + 's']: [this.entityToTemplate(frontmatter, sections)]
}
};
}
}
EntityToTemplateConverter.ts
Convert entity to reusable template:
class EntityToTemplateConverter {
convertEntity(entity: any, entityType: TemplateEntityType): Template {
// Extract variable-worthy fields
const variables = this.extractVariables(entity);
// Replace values with variable placeholders
const templateEntity = this.replaceWithVariables(entity, variables);
return {
id: `entity-${Date.now()}`,
name: `${entity.name} Template`,
category: 'singleEntity',
isBuiltIn: false,
variables,
entities: {
[entityType + 's']: [templateEntity]
}
};
}
}
Template Modals
TemplateLibraryModal.ts
Browse and apply templates:
class TemplateLibraryModal extends ResponsiveModal {
private templates: Template[] = [];
private filters = { category: 'all', showBuiltIn: true, showCustom: true };
onOpen(): void {
// Filter bar
this.renderFilters();
// Search
this.renderSearch();
// Template grid
this.renderTemplateGrid();
}
private renderTemplateGrid(): void {
const filtered = this.applyFilters(this.templates);
for (const template of filtered) {
const card = this.createTemplateCard(template);
card.addEventListener('click', () => this.showTemplateDetail(template));
}
}
}
TemplateVariableEditorModal.ts
Input variable values before applying:
class TemplateVariableEditorModal extends ResponsiveModal {
private template: Template;
private values: TemplateVariableValues = {};
onOpen(): void {
// Title
this.contentEl.createEl('h2', { text: `Apply: ${this.template.name}` });
// Variable inputs
for (const variable of this.template.variables || []) {
this.renderVariableInput(variable);
}
// Preview
this.renderPreview();
// Apply button
this.renderApplyButton();
}
private renderVariableInput(variable: TemplateVariable): void {
new Setting(this.contentEl)
.setName(variable.label)
.setDesc(variable.helpText || '')
.addText(text => {
text.setValue(variable.defaultValue || '')
.onChange(value => {
this.values[variable.name] = value;
this.updatePreview();
});
if (variable.required) {
text.inputEl.required = true;
}
});
}
}
Template Validation
TemplateValidator.ts
class TemplateValidator {
validate(template: Template): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Required fields
if (!template.id) errors.push('Template ID is required');
if (!template.name) errors.push('Template name is required');
// Entity validation
for (const [type, entities] of Object.entries(template.entities)) {
for (const entity of entities) {
if (!entity.templateId) {
errors.push(`Entity in ${type} missing templateId`);
}
if (!entity.name) {
warnings.push(`Entity ${entity.templateId} missing name`);
}
}
}
// Variable usage validation
for (const variable of template.variables || []) {
if (variable.usedIn?.length === 0) {
warnings.push(`Variable "${variable.name}" is not used anywhere`);
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
}
Template Migration
TemplateMigrator.ts
Handle version upgrades:
class TemplateMigrator {
migrate(template: Template, targetVersion: string): Template {
let current = template;
// Apply migrations in order
if (this.versionCompare(current.version, '1.0.0') < 0) {
current = this.migrateTo100(current);
}
if (this.versionCompare(current.version, '1.1.0') < 0) {
current = this.migrateTo110(current);
}
current.version = targetVersion;
return current;
}
}