Storyteller Suite - Template System

Overview

The template system enables:

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;
    }
}