728 lines
19 KiB
Markdown
728 lines
19 KiB
Markdown
|
|
# VFS User Functions - Templates and Examples
|
||
|
|
|
||
|
|
This document provides template functions that you can implement for domain-specific needs. These functions combine VFS primitives to solve common problems.
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
1. [Code Analysis Functions](#code-analysis-functions)
|
||
|
|
2. [Export Format Functions](#export-format-functions)
|
||
|
|
3. [Project Management Functions](#project-management-functions)
|
||
|
|
4. [Creative Writing Functions](#creative-writing-functions)
|
||
|
|
5. [Game Development Functions](#game-development-functions)
|
||
|
|
|
||
|
|
## Code Analysis Functions
|
||
|
|
|
||
|
|
### Get Dependency Graph
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Build a dependency graph for JavaScript/TypeScript projects
|
||
|
|
*/
|
||
|
|
async function getDependencyGraph(vfs, srcPath) {
|
||
|
|
const files = await vfs.readdir(srcPath, { recursive: true })
|
||
|
|
const graph = {}
|
||
|
|
|
||
|
|
for (const file of files) {
|
||
|
|
const filePath = `${srcPath}/${file}`
|
||
|
|
|
||
|
|
// Only process JS/TS files
|
||
|
|
if (file.match(/\.(js|ts|jsx|tsx)$/)) {
|
||
|
|
const content = await vfs.readFile(filePath)
|
||
|
|
const text = content.toString()
|
||
|
|
|
||
|
|
// Parse imports (basic regex, use proper AST parser for production)
|
||
|
|
const imports = []
|
||
|
|
const importRegex = /import\s+.*?\s+from\s+['"](.+?)['"]/g
|
||
|
|
const requireRegex = /require\(['"](.+?)['"]\)/g
|
||
|
|
|
||
|
|
let match
|
||
|
|
while ((match = importRegex.exec(text)) !== null) {
|
||
|
|
imports.push(match[1])
|
||
|
|
}
|
||
|
|
while ((match = requireRegex.exec(text)) !== null) {
|
||
|
|
imports.push(match[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
graph[filePath] = imports
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return graph
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use it
|
||
|
|
const deps = await getDependencyGraph(vfs, '/src')
|
||
|
|
```
|
||
|
|
|
||
|
|
### Find Circular Dependencies
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Detect circular dependencies in your code
|
||
|
|
*/
|
||
|
|
async function findCircularDependencies(vfs, srcPath) {
|
||
|
|
const graph = await getDependencyGraph(vfs, srcPath)
|
||
|
|
const cycles = []
|
||
|
|
|
||
|
|
function detectCycle(node, visited = new Set(), stack = []) {
|
||
|
|
if (stack.includes(node)) {
|
||
|
|
const cycleStart = stack.indexOf(node)
|
||
|
|
cycles.push(stack.slice(cycleStart))
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if (visited.has(node)) return
|
||
|
|
visited.add(node)
|
||
|
|
stack.push(node)
|
||
|
|
|
||
|
|
const dependencies = graph[node] || []
|
||
|
|
for (const dep of dependencies) {
|
||
|
|
// Resolve relative imports
|
||
|
|
const resolvedDep = resolvePath(node, dep)
|
||
|
|
if (graph[resolvedDep]) {
|
||
|
|
detectCycle(resolvedDep, visited, [...stack])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Object.keys(graph).forEach(node => detectCycle(node))
|
||
|
|
return cycles
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Find Untested Code
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Find source files without corresponding test files
|
||
|
|
*/
|
||
|
|
async function findUntestedCode(vfs, srcPath, testPath = null) {
|
||
|
|
testPath = testPath || srcPath.replace('/src', '/tests')
|
||
|
|
|
||
|
|
const sourceFiles = await vfs.readdir(srcPath, { recursive: true })
|
||
|
|
const testFiles = await vfs.readdir(testPath, { recursive: true }).catch(() => [])
|
||
|
|
|
||
|
|
const untestedFiles = []
|
||
|
|
|
||
|
|
for (const sourceFile of sourceFiles) {
|
||
|
|
if (!sourceFile.match(/\.(js|ts|jsx|tsx)$/)) continue
|
||
|
|
|
||
|
|
// Look for corresponding test file
|
||
|
|
const baseName = sourceFile.replace(/\.(js|ts|jsx|tsx)$/, '')
|
||
|
|
const hasTest = testFiles.some(testFile =>
|
||
|
|
testFile.includes(baseName) &&
|
||
|
|
testFile.match(/\.(test|spec)\.(js|ts|jsx|tsx)$/)
|
||
|
|
)
|
||
|
|
|
||
|
|
if (!hasTest) {
|
||
|
|
untestedFiles.push(`${srcPath}/${sourceFile}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return untestedFiles
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Find Similar Code (Duplicate Detection)
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Find potentially duplicate code using similarity scoring
|
||
|
|
*/
|
||
|
|
async function findSimilarCode(vfs, filePath, options = {}) {
|
||
|
|
const threshold = options.threshold || 0.8
|
||
|
|
const searchPath = options.searchPath || '/'
|
||
|
|
|
||
|
|
// Get the reference file content
|
||
|
|
const referenceContent = await vfs.readFile(filePath)
|
||
|
|
const referenceText = referenceContent.toString()
|
||
|
|
|
||
|
|
// Use VFS's semantic search
|
||
|
|
const similar = await vfs.findSimilar(filePath, {
|
||
|
|
limit: 10,
|
||
|
|
threshold
|
||
|
|
})
|
||
|
|
|
||
|
|
// Additionally, do structural comparison
|
||
|
|
const results = []
|
||
|
|
for (const match of similar) {
|
||
|
|
const matchContent = await vfs.readFile(match.path)
|
||
|
|
const matchText = matchContent.toString()
|
||
|
|
|
||
|
|
// Simple line-based similarity (use better algorithms in production)
|
||
|
|
const similarity = calculateSimilarity(referenceText, matchText)
|
||
|
|
|
||
|
|
if (similarity > threshold) {
|
||
|
|
results.push({
|
||
|
|
path: match.path,
|
||
|
|
similarity,
|
||
|
|
semanticScore: match.score
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return results.sort((a, b) => b.similarity - a.similarity)
|
||
|
|
}
|
||
|
|
|
||
|
|
function calculateSimilarity(text1, text2) {
|
||
|
|
// Simple Jaccard similarity on lines
|
||
|
|
const lines1 = new Set(text1.split('\n').map(l => l.trim()).filter(l => l))
|
||
|
|
const lines2 = new Set(text2.split('\n').map(l => l.trim()).filter(l => l))
|
||
|
|
|
||
|
|
const intersection = new Set([...lines1].filter(x => lines2.has(x)))
|
||
|
|
const union = new Set([...lines1, ...lines2])
|
||
|
|
|
||
|
|
return intersection.size / union.size
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Export Format Functions
|
||
|
|
|
||
|
|
### Export to EPUB (for novels)
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Export a directory of markdown files to EPUB format
|
||
|
|
*/
|
||
|
|
async function exportToEpub(vfs, path, metadata = {}) {
|
||
|
|
// First get the markdown export
|
||
|
|
const markdown = await vfs.exportToMarkdown(path)
|
||
|
|
|
||
|
|
// You'll need an EPUB library like epub-gen
|
||
|
|
const Epub = require('epub-gen')
|
||
|
|
|
||
|
|
// Convert markdown chapters to EPUB format
|
||
|
|
const chapters = []
|
||
|
|
const files = await vfs.readdir(path, { recursive: true })
|
||
|
|
|
||
|
|
for (const file of files.sort()) {
|
||
|
|
if (file.endsWith('.md')) {
|
||
|
|
const content = await vfs.readFile(`${path}/${file}`)
|
||
|
|
const title = file.replace('.md', '').replace(/-/g, ' ')
|
||
|
|
|
||
|
|
chapters.push({
|
||
|
|
title: title,
|
||
|
|
data: content.toString()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const options = {
|
||
|
|
title: metadata.title || 'My Book',
|
||
|
|
author: metadata.author || 'Author',
|
||
|
|
chapters: chapters
|
||
|
|
}
|
||
|
|
|
||
|
|
return new Epub(options)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Export to Static Site
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Export VFS content to static HTML site
|
||
|
|
*/
|
||
|
|
async function exportToStaticSite(vfs, sourcePath, options = {}) {
|
||
|
|
const json = await vfs.exportToJSON(sourcePath)
|
||
|
|
const html = []
|
||
|
|
|
||
|
|
html.push('<!DOCTYPE html>')
|
||
|
|
html.push('<html><head>')
|
||
|
|
html.push(`<title>${options.title || 'Documentation'}</title>`)
|
||
|
|
html.push('<style>/* Add your styles */</style>')
|
||
|
|
html.push('</head><body>')
|
||
|
|
|
||
|
|
function renderNode(node, name, depth = 0) {
|
||
|
|
const indent = ' '.repeat(depth)
|
||
|
|
|
||
|
|
if (node._meta?.type === 'file') {
|
||
|
|
html.push(`${indent}<article>`)
|
||
|
|
html.push(`${indent} <h${Math.min(depth + 2, 6)}>${name}</h${Math.min(depth + 2, 6)}>`)
|
||
|
|
|
||
|
|
if (typeof node._content === 'string') {
|
||
|
|
// Convert markdown to HTML if needed
|
||
|
|
html.push(`${indent} <pre>${escapeHtml(node._content)}</pre>`)
|
||
|
|
}
|
||
|
|
|
||
|
|
html.push(`${indent}</article>`)
|
||
|
|
} else if (node._meta?.type === 'directory') {
|
||
|
|
html.push(`${indent}<section>`)
|
||
|
|
html.push(`${indent} <h${Math.min(depth + 1, 6)}>${name}</h${Math.min(depth + 1, 6)}>`)
|
||
|
|
|
||
|
|
for (const [childName, childNode] of Object.entries(node)) {
|
||
|
|
if (!childName.startsWith('_')) {
|
||
|
|
renderNode(childNode, childName, depth + 1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
html.push(`${indent}</section>`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
renderNode(json, options.title || 'Root')
|
||
|
|
|
||
|
|
html.push('</body></html>')
|
||
|
|
return html.join('\n')
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Export to GraphQL Schema
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Generate GraphQL schema from VFS entities
|
||
|
|
*/
|
||
|
|
async function exportToGraphQLSchema(vfs) {
|
||
|
|
const entities = await vfs.listEntities()
|
||
|
|
const types = new Map()
|
||
|
|
|
||
|
|
// Group entities by type
|
||
|
|
for (const entity of entities) {
|
||
|
|
const type = entity.type || 'Unknown'
|
||
|
|
if (!types.has(type)) {
|
||
|
|
types.set(type, [])
|
||
|
|
}
|
||
|
|
types.get(type).push(entity)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate schema
|
||
|
|
let schema = 'type Query {\n'
|
||
|
|
|
||
|
|
for (const [typeName, entities] of types) {
|
||
|
|
schema += ` get${typeName}(id: ID!): ${typeName}\n`
|
||
|
|
schema += ` list${typeName}s: [${typeName}!]!\n`
|
||
|
|
}
|
||
|
|
|
||
|
|
schema += '}\n\n'
|
||
|
|
|
||
|
|
// Generate types
|
||
|
|
for (const [typeName, entities] of types) {
|
||
|
|
schema += `type ${typeName} {\n`
|
||
|
|
schema += ' id: ID!\n'
|
||
|
|
|
||
|
|
// Infer fields from first entity
|
||
|
|
if (entities.length > 0) {
|
||
|
|
const sample = entities[0]
|
||
|
|
for (const [key, value] of Object.entries(sample)) {
|
||
|
|
if (key !== 'id') {
|
||
|
|
const fieldType = inferGraphQLType(value)
|
||
|
|
schema += ` ${key}: ${fieldType}\n`
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
schema += '}\n\n'
|
||
|
|
}
|
||
|
|
|
||
|
|
return schema
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Project Management Functions
|
||
|
|
|
||
|
|
### Get Project Insights
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Analyze project for insights and patterns
|
||
|
|
*/
|
||
|
|
async function getProjectInsights(vfs, projectPath) {
|
||
|
|
const stats = await vfs.getProjectStats(projectPath)
|
||
|
|
const todos = await vfs.getAllTodos(projectPath)
|
||
|
|
const timeline = await vfs.getTimeline({ limit: 100 })
|
||
|
|
|
||
|
|
// Analyze activity patterns
|
||
|
|
const activityByDay = {}
|
||
|
|
const activityByUser = {}
|
||
|
|
const activityByFile = {}
|
||
|
|
|
||
|
|
for (const event of timeline) {
|
||
|
|
const day = event.timestamp.toISOString().split('T')[0]
|
||
|
|
activityByDay[day] = (activityByDay[day] || 0) + 1
|
||
|
|
|
||
|
|
const user = event.user || 'system'
|
||
|
|
activityByUser[user] = (activityByUser[user] || 0) + 1
|
||
|
|
|
||
|
|
activityByFile[event.path] = (activityByFile[event.path] || 0) + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find hotspots (most edited files)
|
||
|
|
const hotspots = Object.entries(activityByFile)
|
||
|
|
.sort(([,a], [,b]) => b - a)
|
||
|
|
.slice(0, 10)
|
||
|
|
.map(([path, count]) => ({ path, edits: count }))
|
||
|
|
|
||
|
|
// Todo analysis
|
||
|
|
const todosByPriority = {}
|
||
|
|
const todosByStatus = {}
|
||
|
|
|
||
|
|
for (const todo of todos) {
|
||
|
|
todosByPriority[todo.priority] = (todosByPriority[todo.priority] || 0) + 1
|
||
|
|
todosByStatus[todo.status] = (todosByStatus[todo.status] || 0) + 1
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
stats,
|
||
|
|
activity: {
|
||
|
|
byDay: activityByDay,
|
||
|
|
byUser: activityByUser,
|
||
|
|
hotspots
|
||
|
|
},
|
||
|
|
todos: {
|
||
|
|
total: todos.length,
|
||
|
|
byPriority: todosByPriority,
|
||
|
|
byStatus: todosByStatus,
|
||
|
|
highPriority: todos.filter(t => t.priority === 'high' && t.status === 'pending')
|
||
|
|
},
|
||
|
|
recommendations: generateRecommendations(stats, todos, hotspots)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function generateRecommendations(stats, todos, hotspots) {
|
||
|
|
const recommendations = []
|
||
|
|
|
||
|
|
if (stats.largestFile && stats.largestFile.size > 1024 * 1024) {
|
||
|
|
recommendations.push({
|
||
|
|
type: 'refactor',
|
||
|
|
message: `Consider splitting ${stats.largestFile.path} (${Math.round(stats.largestFile.size / 1024)}KB)`
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
if (todos.filter(t => t.priority === 'high' && t.status === 'pending').length > 5) {
|
||
|
|
recommendations.push({
|
||
|
|
type: 'priority',
|
||
|
|
message: 'You have many high-priority pending todos'
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
if (hotspots.length > 0 && hotspots[0].edits > 50) {
|
||
|
|
recommendations.push({
|
||
|
|
type: 'stability',
|
||
|
|
message: `${hotspots[0].path} changes frequently, consider stabilizing`
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return recommendations
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Generate Sprint Report
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Generate a report for the current sprint
|
||
|
|
*/
|
||
|
|
async function generateSprintReport(vfs, sprintStart, sprintEnd = new Date()) {
|
||
|
|
const timeline = await vfs.getTimeline({
|
||
|
|
from: sprintStart,
|
||
|
|
to: sprintEnd
|
||
|
|
})
|
||
|
|
|
||
|
|
const todos = await vfs.getAllTodos()
|
||
|
|
|
||
|
|
// Group work by user
|
||
|
|
const workByUser = {}
|
||
|
|
for (const event of timeline) {
|
||
|
|
const user = event.user || 'system'
|
||
|
|
if (!workByUser[user]) {
|
||
|
|
workByUser[user] = {
|
||
|
|
commits: 0,
|
||
|
|
filesModified: new Set(),
|
||
|
|
linesChanged: 0
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
workByUser[user].commits++
|
||
|
|
workByUser[user].filesModified.add(event.path)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Calculate completion rate
|
||
|
|
const completedTodos = todos.filter(t => t.status === 'completed').length
|
||
|
|
const totalTodos = todos.length
|
||
|
|
const completionRate = totalTodos > 0 ? (completedTodos / totalTodos * 100).toFixed(1) : 0
|
||
|
|
|
||
|
|
return {
|
||
|
|
period: {
|
||
|
|
start: sprintStart,
|
||
|
|
end: sprintEnd,
|
||
|
|
days: Math.ceil((sprintEnd - sprintStart) / (1000 * 60 * 60 * 24))
|
||
|
|
},
|
||
|
|
team: Object.entries(workByUser).map(([user, work]) => ({
|
||
|
|
user,
|
||
|
|
commits: work.commits,
|
||
|
|
filesModified: work.filesModified.size
|
||
|
|
})),
|
||
|
|
todos: {
|
||
|
|
completed: completedTodos,
|
||
|
|
total: totalTodos,
|
||
|
|
completionRate: `${completionRate}%`,
|
||
|
|
remaining: todos.filter(t => t.status === 'pending')
|
||
|
|
},
|
||
|
|
velocity: {
|
||
|
|
commitsPerDay: (timeline.length / 7).toFixed(1),
|
||
|
|
todosPerDay: (completedTodos / 7).toFixed(1)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Creative Writing Functions
|
||
|
|
|
||
|
|
### Track Character Arcs
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Track how characters evolve throughout a story
|
||
|
|
*/
|
||
|
|
async function trackCharacterArc(vfs, characterName, storyPath = '/') {
|
||
|
|
// Find the character entity
|
||
|
|
const entities = await vfs.searchEntities({
|
||
|
|
type: 'character',
|
||
|
|
name: characterName
|
||
|
|
})
|
||
|
|
|
||
|
|
if (entities.length === 0) {
|
||
|
|
throw new Error(`Character ${characterName} not found`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const character = entities[0]
|
||
|
|
const occurrences = await vfs.findEntityOccurrences(character.id)
|
||
|
|
|
||
|
|
// Analyze each appearance
|
||
|
|
const arc = []
|
||
|
|
|
||
|
|
for (const occurrence of occurrences) {
|
||
|
|
const content = await vfs.readFile(occurrence.path)
|
||
|
|
const text = content.toString()
|
||
|
|
|
||
|
|
// Find mentions of the character (basic approach)
|
||
|
|
const mentions = text.split('\n').filter(line =>
|
||
|
|
line.toLowerCase().includes(characterName.toLowerCase())
|
||
|
|
)
|
||
|
|
|
||
|
|
arc.push({
|
||
|
|
chapter: occurrence.path,
|
||
|
|
mentions: mentions.length,
|
||
|
|
// Analyze emotional tone (simplified)
|
||
|
|
mood: analyzeMood(mentions),
|
||
|
|
// Extract key actions
|
||
|
|
actions: extractActions(mentions, characterName)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
character: character.metadata,
|
||
|
|
arc: arc,
|
||
|
|
summary: summarizeArc(arc)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function analyzeMood(mentions) {
|
||
|
|
const positiveWords = ['smiled', 'laughed', 'happy', 'joy', 'love', 'success']
|
||
|
|
const negativeWords = ['cried', 'angry', 'sad', 'fear', 'fail', 'death']
|
||
|
|
|
||
|
|
let positive = 0, negative = 0
|
||
|
|
|
||
|
|
for (const mention of mentions) {
|
||
|
|
const lower = mention.toLowerCase()
|
||
|
|
positive += positiveWords.filter(w => lower.includes(w)).length
|
||
|
|
negative += negativeWords.filter(w => lower.includes(w)).length
|
||
|
|
}
|
||
|
|
|
||
|
|
if (positive > negative) return 'positive'
|
||
|
|
if (negative > positive) return 'negative'
|
||
|
|
return 'neutral'
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Generate Story Bible
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Create a comprehensive reference for your story universe
|
||
|
|
*/
|
||
|
|
async function generateStoryBible(vfs, storyPath) {
|
||
|
|
const characters = await vfs.listEntities({ type: 'character' })
|
||
|
|
const locations = await vfs.listEntities({ type: 'location' })
|
||
|
|
const concepts = await vfs.findConcepts({ domain: 'narrative' })
|
||
|
|
|
||
|
|
const bible = {
|
||
|
|
title: 'Story Bible',
|
||
|
|
generated: new Date(),
|
||
|
|
characters: {},
|
||
|
|
locations: {},
|
||
|
|
plotThreads: {},
|
||
|
|
timeline: []
|
||
|
|
}
|
||
|
|
|
||
|
|
// Document characters
|
||
|
|
for (const char of characters) {
|
||
|
|
const occurrences = await vfs.findEntityOccurrences(char.id)
|
||
|
|
bible.characters[char.metadata.name] = {
|
||
|
|
...char.metadata,
|
||
|
|
appearances: occurrences.map(o => o.path),
|
||
|
|
relationships: await vfs.getEntityGraph(char.id, { depth: 1 })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Document locations
|
||
|
|
for (const loc of locations) {
|
||
|
|
bible.locations[loc.metadata.name] = {
|
||
|
|
...loc.metadata,
|
||
|
|
scenes: await vfs.findEntityOccurrences(loc.id)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Plot threads from concepts
|
||
|
|
for (const concept of concepts) {
|
||
|
|
if (concept.type === 'plot') {
|
||
|
|
bible.plotThreads[concept.name] = {
|
||
|
|
description: concept.description,
|
||
|
|
keywords: concept.keywords,
|
||
|
|
chapters: await vfs.findByConcept(concept.name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate timeline
|
||
|
|
const events = await vfs.getTimeline({ limit: 1000 })
|
||
|
|
bible.timeline = events.map(e => ({
|
||
|
|
date: e.timestamp,
|
||
|
|
event: e.description,
|
||
|
|
chapter: e.path
|
||
|
|
}))
|
||
|
|
|
||
|
|
return bible
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Game Development Functions
|
||
|
|
|
||
|
|
### Validate Game Data
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Validate game configuration files for consistency
|
||
|
|
*/
|
||
|
|
async function validateGameData(vfs, gamePath) {
|
||
|
|
const errors = []
|
||
|
|
const warnings = []
|
||
|
|
|
||
|
|
// Load all game data
|
||
|
|
const gameData = await vfs.exportToJSON(gamePath)
|
||
|
|
|
||
|
|
// Check quest references
|
||
|
|
if (gameData.quests) {
|
||
|
|
for (const [questName, quest] of Object.entries(gameData.quests)) {
|
||
|
|
// Check NPC references
|
||
|
|
if (quest._content?.questGiver) {
|
||
|
|
const npcPath = `${gamePath}/npcs/${quest._content.questGiver}.json`
|
||
|
|
const exists = await vfs.exists(npcPath)
|
||
|
|
if (!exists) {
|
||
|
|
errors.push(`Quest ${questName} references missing NPC: ${quest._content.questGiver}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check item rewards
|
||
|
|
if (quest._content?.rewards?.items) {
|
||
|
|
for (const item of quest._content.rewards.items) {
|
||
|
|
const itemPath = `${gamePath}/items/${item}.json`
|
||
|
|
const exists = await vfs.exists(itemPath)
|
||
|
|
if (!exists) {
|
||
|
|
warnings.push(`Quest ${questName} rewards missing item: ${item}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check balance
|
||
|
|
if (gameData.items) {
|
||
|
|
const itemPowers = []
|
||
|
|
for (const [itemName, item] of Object.entries(gameData.items)) {
|
||
|
|
if (item._content?.stats) {
|
||
|
|
const totalPower = Object.values(item._content.stats)
|
||
|
|
.reduce((a, b) => a + b, 0)
|
||
|
|
itemPowers.push({ name: itemName, power: totalPower })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find outliers
|
||
|
|
const avgPower = itemPowers.reduce((a, b) => a + b.power, 0) / itemPowers.length
|
||
|
|
const outliers = itemPowers.filter(i => Math.abs(i.power - avgPower) > avgPower * 2)
|
||
|
|
|
||
|
|
for (const outlier of outliers) {
|
||
|
|
warnings.push(`Item ${outlier.name} may be imbalanced (power: ${outlier.power}, avg: ${avgPower})`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return { errors, warnings, valid: errors.length === 0 }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Generate Loot Tables
|
||
|
|
```javascript
|
||
|
|
/**
|
||
|
|
* Generate weighted loot tables from item definitions
|
||
|
|
*/
|
||
|
|
async function generateLootTables(vfs, itemsPath) {
|
||
|
|
const items = await vfs.exportToJSON(itemsPath)
|
||
|
|
const tables = {
|
||
|
|
common: [],
|
||
|
|
uncommon: [],
|
||
|
|
rare: [],
|
||
|
|
epic: [],
|
||
|
|
legendary: []
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const [itemName, item] of Object.entries(items)) {
|
||
|
|
if (item._meta?.type === 'file' && item._content?.rarity) {
|
||
|
|
const entry = {
|
||
|
|
name: itemName.replace('.json', ''),
|
||
|
|
weight: getWeight(item._content.rarity),
|
||
|
|
data: item._content
|
||
|
|
}
|
||
|
|
|
||
|
|
tables[item._content.rarity.toLowerCase()].push(entry)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Normalize weights
|
||
|
|
for (const table of Object.values(tables)) {
|
||
|
|
const totalWeight = table.reduce((a, b) => a + b.weight, 0)
|
||
|
|
for (const entry of table) {
|
||
|
|
entry.probability = (entry.weight / totalWeight * 100).toFixed(2) + '%'
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return tables
|
||
|
|
}
|
||
|
|
|
||
|
|
function getWeight(rarity) {
|
||
|
|
const weights = {
|
||
|
|
common: 100,
|
||
|
|
uncommon: 50,
|
||
|
|
rare: 20,
|
||
|
|
epic: 5,
|
||
|
|
legendary: 1
|
||
|
|
}
|
||
|
|
return weights[rarity.toLowerCase()] || 10
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Using These Functions
|
||
|
|
|
||
|
|
All these functions are templates that you can customize for your specific needs. To use them:
|
||
|
|
|
||
|
|
1. Copy the function you need
|
||
|
|
2. Modify it for your specific requirements
|
||
|
|
3. Use it with your VFS instance:
|
||
|
|
|
||
|
|
```javascript
|
||
|
|
import { Brainy } from '@soulcraft/brainy'
|
||
|
|
|
||
|
|
// Initialize VFS
|
||
|
|
const brain = new Brainy()
|
||
|
|
await brain.init()
|
||
|
|
const vfs = brain.vfs()
|
||
|
|
await vfs.init()
|
||
|
|
|
||
|
|
// Use your custom function
|
||
|
|
const insights = await getProjectInsights(vfs, '/my-project')
|
||
|
|
console.log(insights.recommendations)
|
||
|
|
|
||
|
|
// Combine multiple functions
|
||
|
|
const deps = await getDependencyGraph(vfs, '/src')
|
||
|
|
const cycles = await findCircularDependencies(vfs, '/src')
|
||
|
|
const untested = await findUntestedCode(vfs, '/src', '/tests')
|
||
|
|
```
|
||
|
|
|
||
|
|
Remember: These are starting points. The power of VFS is that you can combine its primitives to build exactly what you need for your domain!
|