-
Notifications
You must be signed in to change notification settings - Fork 107
Open
Description
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
}
}Metadata
Metadata
Assignees
Labels
No labels