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
- Custom math functions: Game-specific calculations (e.g.,
diminishingReturns(), softCap())
- Random systems: Weighted random, seeded random, gacha rates
- Data lookups: Table queries, database access
- External integrations: API calls, analytics hooks
- 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
}
}
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
diminishingReturns(),softCap())Proposed Plugin API
1. Plugin Interface
2. Plugin Manager
3. Example Plugins
RPG Utilities Plugin
Gacha Plugin
4. Integration with fx
5. TypeScript Support