Skip to content

Plugin API for extending formula functions #15

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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions