Skip to content

Plugin API for extending formula functions #15

@GameLooks

Description

@GameLooks

Motivation

Game developers often need custom functions beyond the built-in math utilities. Currently, adding new functions requires modifying the engine source code. A plugin API would allow users to extend functionality without forking the repository.

Use Cases

  1. Custom math functions: Game-specific calculations (e.g., diminishingReturns(), softCap())
  2. Random systems: Weighted random, seeded random, gacha rates
  3. Data lookups: Table queries, database access
  4. External integrations: API calls, analytics hooks
  5. Domain-specific logic: Card game mechanics, combat calculations

Proposed Plugin API

1. Plugin Interface

// src/plugins/types.ts
export interface FxPlugin {
  /** Unique plugin identifier */
  name: string

  /** Semantic version */
  version: string

  /** Optional plugin dependencies */
  dependencies?: string[]

  /** Called when plugin is registered */
  install?(context: PluginContext): void | Promise<void>

  /** Called when plugin is uninstalled */
  uninstall?(context: PluginContext): void

  /** Custom functions to register */
  functions?: Record<string, FormulaFunction>

  /** Custom operators to register */
  operators?: Record<string, OperatorDefinition>

  /** Event listeners */
  listeners?: Record<string, EventHandler>
}

export interface FormulaFunction {
  /** Function implementation */
  execute: (...args: number[]) => number

  /** Minimum arguments required */
  minArgs?: number

  /** Maximum arguments allowed */
  maxArgs?: number

  /** Description for documentation */
  description?: string

  /** Example usage */
  example?: string
}

export interface PluginContext {
  /** Access to fx singleton */
  fx: typeof fx

  /** Logger instance */
  logger: Logger

  /** Plugin configuration */
  config: Record<string, unknown>
}

2. Plugin Manager

// src/plugins/PluginManager.ts
export class PluginManager {
  private plugins: Map<string, FxPlugin> = new Map()
  private functions: Map<string, FormulaFunction> = new Map()

  async register(plugin: FxPlugin): Promise<void> {
    // Validate plugin
    this.validatePlugin(plugin)

    // Check dependencies
    await this.resolveDependencies(plugin)

    // Install plugin
    const context = this.createContext(plugin)
    await plugin.install?.(context)

    // Register functions
    if (plugin.functions) {
      for (const [name, fn] of Object.entries(plugin.functions)) {
        this.registerFunction(`${plugin.name}:${name}`, fn)
        // Also register without namespace for convenience
        if (!this.functions.has(name)) {
          this.registerFunction(name, fn)
        }
      }
    }

    // Register event listeners
    if (plugin.listeners) {
      for (const [event, handler] of Object.entries(plugin.listeners)) {
        fx.on(event, handler)
      }
    }

    this.plugins.set(plugin.name, plugin)
    fx.emit('plugin:registered', { name: plugin.name })
  }

  unregister(pluginName: string): void {
    const plugin = this.plugins.get(pluginName)
    if (!plugin) return

    // Uninstall
    plugin.uninstall?.(this.createContext(plugin))

    // Remove functions
    if (plugin.functions) {
      for (const name of Object.keys(plugin.functions)) {
        this.functions.delete(`${pluginName}:${name}`)
      }
    }

    this.plugins.delete(pluginName)
    fx.emit('plugin:unregistered', { name: pluginName })
  }

  getFunction(name: string): FormulaFunction | undefined {
    return this.functions.get(name)
  }

  listPlugins(): string[] {
    return Array.from(this.plugins.keys())
  }
}

3. Example Plugins

RPG Utilities Plugin

const rpgPlugin: FxPlugin = {
  name: 'rpg-utils',
  version: '1.0.0',

  functions: {
    // Soft cap with diminishing returns
    softCap: {
      execute: (value, cap, rate = 0.5) => {
        if (value <= cap) return value
        const overflow = value - cap
        return cap + overflow * rate
      },
      minArgs: 2,
      maxArgs: 3,
      description: 'Apply soft cap with diminishing returns',
      example: 'softCap(150, 100, 0.5) // Returns 125'
    },

    // Percentage chance check
    chance: {
      execute: (percentage) => Math.random() * 100 < percentage ? 1 : 0,
      minArgs: 1,
      maxArgs: 1,
      description: 'Returns 1 if random check passes, 0 otherwise',
      example: 'chance(25) // 25% chance to return 1'
    },

    // Weighted random selection
    weightedRandom: {
      execute: (...weights) => {
        const total = weights.reduce((a, b) => a + b, 0)
        let random = Math.random() * total
        for (let i = 0; i < weights.length; i++) {
          random -= weights[i]
          if (random <= 0) return i
        }
        return weights.length - 1
      },
      minArgs: 2,
      description: 'Select index based on weights',
      example: 'weightedRandom(70, 20, 10) // 70% chance of 0'
    }
  }
}

// Register
fx.plugins.register(rpgPlugin)

// Use in formulas
fx.evaluate('softCap(ATK, 100, 0.5)')
fx.evaluate('baseDamage * (1 + chance(critRate) * critBonus)')

Gacha Plugin

const gachaPlugin: FxPlugin = {
  name: 'gacha',
  version: '1.0.0',

  functions: {
    // Pity system calculation
    pityRate: {
      execute: (baseRate, pullCount, pityStart, pityIncrement) => {
        if (pullCount < pityStart) return baseRate
        const pityPulls = pullCount - pityStart
        return Math.min(100, baseRate + pityPulls * pityIncrement)
      },
      minArgs: 4,
      description: 'Calculate rate with pity system'
    },

    // Guaranteed pity
    isPity: {
      execute: (pullCount, hardPity) => pullCount >= hardPity ? 1 : 0,
      minArgs: 2,
      description: 'Check if at hard pity'
    }
  }
}

4. Integration with fx

// Register via fx singleton
fx.use(rpgPlugin)
fx.use(gachaPlugin)

// Or with configuration
fx.use(myPlugin, { configOption: 'value' })

// List active plugins
console.log(fx.plugins.list())  // ['rpg-utils', 'gacha']

// Check if function exists
fx.hasFunction('softCap')  // true

// Get function metadata
const meta = fx.getFunctionMeta('softCap')
console.log(meta.description)

5. TypeScript Support

// Extend type definitions
declare module '@soonfx/core' {
  interface FormulaFunctions {
    softCap(value: number, cap: number, rate?: number): number
    chance(percentage: number): 0 | 1
    pityRate(base: number, pulls: number, start: number, inc: number): number
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions